Add support for sending media within threads.

This patch add the `threadRootEventID` as a parameter to media sending functions and forwards the timeline thread root between various application states to the place where its needed.
The 2 missing items are Locations and Polls which currently don't have any support on the Rust side for being sent within threads. That will come in a separate PR.
This commit is contained in:
Stefan Ceriu
2025-06-10 17:18:32 +03:00
committed by Stefan Ceriu
parent 6481093107
commit 558cdf5be3
18 changed files with 340 additions and 247 deletions
@@ -301,8 +301,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (_, .presentReportContent, .reportContent(let itemID, let senderID, _)): case (_, .presentReportContent, .reportContent(let itemID, let senderID, _)):
presentReportContent(for: itemID, from: senderID) presentReportContent(for: itemID, from: senderID)
case (_, .presentMediaUploadPicker, .mediaUploadPicker(let source, _)): case (_, .presentMediaUploadPicker, .mediaUploadPicker(let source, let threadRootEventID, _)):
presentMediaUploadPickerWithSource(source) presentMediaUploadPickerWithSource(source, threadRootEventID: threadRootEventID)
case (_, .presentEmojiPicker, .emojiPicker(let itemID, let selectedEmoji, _)): case (_, .presentEmojiPicker, .emojiPicker(let itemID, let selectedEmoji, _)):
presentEmojiPicker(for: itemID, selectedEmoji: selectedEmoji) presentEmojiPicker(for: itemID, selectedEmoji: selectedEmoji)
@@ -387,8 +387,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.pollsHistory, .presentPollForm(let mode), .pollsHistoryForm): case (.pollsHistory, .presentPollForm(let mode), .pollsHistoryForm):
presentPollForm(mode: mode) presentPollForm(mode: mode)
case (_, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL, _)): case (_, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL, let threadRootEventID, _)):
presentMediaUploadPreviewScreen(for: fileURL, animated: animated) presentMediaUploadPreviewScreen(for: fileURL, threadRootEventID: threadRootEventID, animated: animated)
case (_, .presentInviteUsersScreen, .inviteUsersScreen): case (_, .presentInviteUsersScreen, .inviteUsersScreen):
presentInviteUsersScreen() presentInviteUsersScreen()
@@ -441,7 +441,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .eventFocus(let focusedEvent): case .eventFocus(let focusedEvent):
roomScreenCoordinator?.focusOnEvent(focusedEvent) roomScreenCoordinator?.focusOnEvent(focusedEvent)
case .share(.mediaFile(_, let mediaFile)): case .share(.mediaFile(_, let mediaFile)):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated)) stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url, threadRootEventID: nil),
userInfo: EventUserInfo(animated: animated))
case .share(.text(_, let text)): case .share(.text(_, let text)):
roomScreenCoordinator?.shareText(text) roomScreenCoordinator?.shareText(text)
case .none: case .none:
@@ -477,7 +478,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch presentationAction { switch presentationAction {
case .share(.mediaFile(_, let mediaFile)): case .share(.mediaFile(_, let mediaFile)):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated)) stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url, threadRootEventID: nil),
userInfo: EventUserInfo(animated: animated))
case .share(.text), .eventFocus: case .share(.text), .eventFocus:
break // These are both handled in the coordinator's init. break // These are both handled in the coordinator's init.
case .none: case .none:
@@ -528,9 +530,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .presentReportContent(let itemID, let senderID): case .presentReportContent(let itemID, let senderID):
stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID)) stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID))
case .presentMediaUploadPicker(let source): case .presentMediaUploadPicker(let source):
stateMachine.tryEvent(.presentMediaUploadPicker(source: source)) stateMachine.tryEvent(.presentMediaUploadPicker(source: source, threadRootEventID: nil))
case .presentMediaUploadPreviewScreen(let url): case .presentMediaUploadPreviewScreen(let url):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url)) stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url, threadRootEventID: nil))
case .presentEmojiPicker(let itemID, let selectedEmojis): case .presentEmojiPicker(let itemID, let selectedEmojis):
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
case .presentLocationPicker: case .presentLocationPicker:
@@ -605,10 +607,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .presentReportContent(let itemID, let senderID): case .presentReportContent(let itemID, let senderID):
stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID)) stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID))
case .presentMediaUploadPicker(let source): case .presentMediaUploadPicker(let source, let threadRootEventID):
stateMachine.tryEvent(.presentMediaUploadPicker(source: source)) stateMachine.tryEvent(.presentMediaUploadPicker(source: source,
case .presentMediaUploadPreviewScreen(let url): threadRootEventID: threadRootEventID))
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url)) case .presentMediaUploadPreviewScreen(let url, let threadRootEventID):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url,
threadRootEventID: threadRootEventID))
case .presentLocationPicker: case .presentLocationPicker:
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker)) stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker))
case .presentPollForm(let mode): case .presentPollForm(let mode):
@@ -867,7 +871,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func presentMediaUploadPickerWithSource(_ source: MediaPickerScreenSource) { private func presentMediaUploadPickerWithSource(_ source: MediaPickerScreenSource,
threadRootEventID: String?) {
let stackCoordinator = NavigationStackCoordinator() let stackCoordinator = NavigationStackCoordinator()
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController, let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController,
@@ -880,7 +885,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .cancel: case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
case .selectMediaAtURL(let url): case .selectMediaAtURL(let url):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url)) stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url, threadRootEventID: threadRootEventID))
} }
} }
@@ -893,7 +898,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func presentMediaUploadPreviewScreen(for url: URL, animated: Bool) { private func presentMediaUploadPreviewScreen(for url: URL,
threadRootEventID: String?,
animated: Bool) {
let stackCoordinator = NavigationStackCoordinator() let stackCoordinator = NavigationStackCoordinator()
let parameters = MediaUploadPreviewScreenCoordinatorParameters(userIndicatorController: userIndicatorController, let parameters = MediaUploadPreviewScreenCoordinatorParameters(userIndicatorController: userIndicatorController,
@@ -901,6 +908,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings), mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
title: url.lastPathComponent, title: url.lastPathComponent,
url: url, url: url,
threadRootEventID: threadRootEventID,
shouldShowCaptionWarning: appSettings.shouldShowMediaCaptionWarning) shouldShowCaptionWarning: appSettings.shouldShowMediaCaptionWarning)
let mediaUploadPreviewScreenCoordinator = MediaUploadPreviewScreenCoordinator(parameters: parameters) let mediaUploadPreviewScreenCoordinator = MediaUploadPreviewScreenCoordinator(parameters: parameters)
@@ -966,11 +974,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .selectedLocation(let geoURI, let isUserLocation): case .selectedLocation(let geoURI, let isUserLocation):
Task { Task {
#warning("Allow sending locations within threads when the SDK allows it")
_ = await self.roomProxy.timeline.sendLocation(body: geoURI.bodyMessage, _ = await self.roomProxy.timeline.sendLocation(body: geoURI.bodyMessage,
geoURI: geoURI, geoURI: geoURI,
description: nil, description: nil,
zoomLevel: 15, zoomLevel: 15,
assetType: isUserLocation ? .sender : .pin) assetType: isUserLocation ? .sender : .pin,
threadRootEventID: nil)
self.navigationStackCoordinator.setSheetCoordinator(nil) self.navigationStackCoordinator.setSheetCoordinator(nil)
} }
@@ -1028,7 +1038,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private func createPoll(question: String, options: [String], pollKind: Poll.Kind) { private func createPoll(question: String, options: [String], pollKind: Poll.Kind) {
Task { Task {
let result = await roomProxy.timeline.createPoll(question: question, answers: options, pollKind: pollKind) #warning("Allow sending polls within threads when the SDK allows it")
let result = await roomProxy.timeline.createPoll(question: question, answers: options, pollKind: pollKind, threadRootEventID: nil)
self.analytics.trackComposer(inThread: false, self.analytics.trackComposer(inThread: false,
isEditing: false, isEditing: false,
@@ -57,8 +57,8 @@ extension RoomFlowCoordinator {
case roomMemberDetails(userID: String, previousState: State) case roomMemberDetails(userID: String, previousState: State)
case userProfile(userID: String, previousState: State) case userProfile(userID: String, previousState: State)
case inviteUsersScreen(previousState: State) case inviteUsersScreen(previousState: State)
case mediaUploadPicker(source: MediaPickerScreenSource, previousState: State) case mediaUploadPicker(source: MediaPickerScreenSource, threadRootEventID: String?, previousState: State)
case mediaUploadPreview(fileURL: URL, previousState: State) case mediaUploadPreview(fileURL: URL, threadRootEventID: String?, previousState: State)
case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>, previousState: State) case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>, previousState: State)
case mapNavigator(previousState: State) case mapNavigator(previousState: State)
case messageForwarding(forwardingItem: MessageForwardingItem, previousState: State) case messageForwarding(forwardingItem: MessageForwardingItem, previousState: State)
@@ -122,10 +122,10 @@ extension RoomFlowCoordinator {
case presentInviteUsersScreen case presentInviteUsersScreen
case dismissInviteUsersScreen case dismissInviteUsersScreen
case presentMediaUploadPicker(source: MediaPickerScreenSource) case presentMediaUploadPicker(source: MediaPickerScreenSource, threadRootEventID: String?)
case dismissMediaUploadPicker case dismissMediaUploadPicker
case presentMediaUploadPreview(fileURL: URL) case presentMediaUploadPreview(fileURL: URL, threadRootEventID: String?)
case dismissMediaUploadPreview case dismissMediaUploadPreview
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>) case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
@@ -184,11 +184,11 @@ extension RoomFlowCoordinator {
case (.room, .presentReportContent(let itemID, let senderID)): case (.room, .presentReportContent(let itemID, let senderID)):
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState) return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
case (.room, .presentMediaUploadPicker(let source)): case (.room, .presentMediaUploadPicker(let source, let threadRootEventID)):
return .mediaUploadPicker(source: source, previousState: fromState) return .mediaUploadPicker(source: source, threadRootEventID: threadRootEventID, previousState: fromState)
case (.room, .presentMediaUploadPreview(let fileURL)): case (.room, .presentMediaUploadPreview(let fileURL, let threadRootEventID)):
return .mediaUploadPreview(fileURL: fileURL, previousState: fromState) return .mediaUploadPreview(fileURL: fileURL, threadRootEventID: threadRootEventID, previousState: fromState)
case (.room, .presentEmojiPicker(let itemID, let selectedEmoji)): case (.room, .presentEmojiPicker(let itemID, let selectedEmoji)):
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState) return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
@@ -221,11 +221,11 @@ extension RoomFlowCoordinator {
case (.thread, .presentReportContent(let itemID, let senderID)): case (.thread, .presentReportContent(let itemID, let senderID)):
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState) return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
case (.thread, .presentMediaUploadPicker(let source)): case (.thread, .presentMediaUploadPicker(let source, let threadRootEventID)):
return .mediaUploadPicker(source: source, previousState: fromState) return .mediaUploadPicker(source: source, threadRootEventID: threadRootEventID, previousState: fromState)
case (.thread, .presentMediaUploadPreview(let fileURL)): case (.thread, .presentMediaUploadPreview(let fileURL, let threadRootEventID)):
return .mediaUploadPreview(fileURL: fileURL, previousState: fromState) return .mediaUploadPreview(fileURL: fileURL, threadRootEventID: threadRootEventID, previousState: fromState)
case (.thread, .presentEmojiPicker(let itemID, let selectedEmoji)): case (.thread, .presentEmojiPicker(let itemID, let selectedEmoji)):
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState) return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
@@ -244,7 +244,7 @@ extension RoomFlowCoordinator {
// Room + Thread // Room + Thread
case (.mediaUploadPicker(_, let previousState), .dismissMediaUploadPicker): case (.mediaUploadPicker(_, _, let previousState), .dismissMediaUploadPicker):
return previousState return previousState
case (.emojiPicker(_, _, let previouState), .dismissEmojiPicker): case (.emojiPicker(_, _, let previouState), .dismissEmojiPicker):
@@ -343,7 +343,7 @@ extension RoomFlowCoordinator {
case (.knockRequestsList(let previousState), .dismissKnockRequestsListScreen): case (.knockRequestsList(let previousState), .dismissKnockRequestsListScreen):
return previousState return previousState
case (.mediaUploadPreview(_, let previousState), .dismissMediaUploadPreview): case (.mediaUploadPreview(_, _, let previousState), .dismissMediaUploadPreview):
return previousState return previousState
case (.notificationSettings, .presentGlobalNotificationSettingsScreen): case (.notificationSettings, .presentGlobalNotificationSettingsScreen):
@@ -361,8 +361,8 @@ extension RoomFlowCoordinator {
case (.pollsHistoryForm, .dismissPollForm): case (.pollsHistoryForm, .dismissPollForm):
return .pollsHistory return .pollsHistory
case (.mediaUploadPicker(_, let previousMediaUploadPickerState), .presentMediaUploadPreview(let fileURL)): case (.mediaUploadPicker(_, _, let previousMediaUploadPickerState), .presentMediaUploadPreview(let fileURL, let threadRootEventID)):
return .mediaUploadPreview(fileURL: fileURL, previousState: previousMediaUploadPickerState) return .mediaUploadPreview(fileURL: fileURL, threadRootEventID: threadRootEventID, previousState: previousMediaUploadPickerState)
case (_, .presentInviteUsersScreen): case (_, .presentInviteUsersScreen):
return .inviteUsersScreen(previousState: fromState) return .inviteUsersScreen(previousState: fromState)
@@ -16153,15 +16153,15 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
//MARK: - sendAudio //MARK: - sendAudio
var sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount = 0 var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendAudioUrlAudioInfoCaptionRequestHandleCallsCount: Int { var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount return sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount returnValue = sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -16169,27 +16169,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount = newValue sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount = newValue sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} }
} }
} }
} }
var sendAudioUrlAudioInfoCaptionRequestHandleCalled: Bool { var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleCalled: Bool {
return sendAudioUrlAudioInfoCaptionRequestHandleCallsCount > 0 return sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleCallsCount > 0
} }
var sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>! var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendAudioUrlAudioInfoCaptionRequestHandleReturnValue: Result<Void, TimelineProxyError>! { var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue return sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} else { } else {
var returnValue: Result<Void, TimelineProxyError>? = nil var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue returnValue = sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -16197,35 +16197,35 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue = newValue sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue = newValue sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} }
} }
} }
} }
var sendAudioUrlAudioInfoCaptionRequestHandleClosure: ((URL, AudioInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)? var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure: ((URL, AudioInfo, String?, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendAudio(url: URL, audioInfo: AudioInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { func sendAudio(url: URL, audioInfo: AudioInfo, caption: String?, threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendAudioUrlAudioInfoCaptionRequestHandleCallsCount += 1 sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleCallsCount += 1
if let sendAudioUrlAudioInfoCaptionRequestHandleClosure = sendAudioUrlAudioInfoCaptionRequestHandleClosure { if let sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure = sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure {
return await sendAudioUrlAudioInfoCaptionRequestHandleClosure(url, audioInfo, caption, requestHandle) return await sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure(url, audioInfo, caption, threadRootEventID, requestHandle)
} else { } else {
return sendAudioUrlAudioInfoCaptionRequestHandleReturnValue return sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleReturnValue
} }
} }
//MARK: - sendFile //MARK: - sendFile
var sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount = 0 var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendFileUrlFileInfoCaptionRequestHandleCallsCount: Int { var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount return sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount returnValue = sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -16233,27 +16233,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount = newValue sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount = newValue sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} }
} }
} }
} }
var sendFileUrlFileInfoCaptionRequestHandleCalled: Bool { var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleCalled: Bool {
return sendFileUrlFileInfoCaptionRequestHandleCallsCount > 0 return sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleCallsCount > 0
} }
var sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>! var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendFileUrlFileInfoCaptionRequestHandleReturnValue: Result<Void, TimelineProxyError>! { var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue return sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} else { } else {
var returnValue: Result<Void, TimelineProxyError>? = nil var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue returnValue = sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -16261,35 +16261,35 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue = newValue sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue = newValue sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} }
} }
} }
} }
var sendFileUrlFileInfoCaptionRequestHandleClosure: ((URL, FileInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)? var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure: ((URL, FileInfo, String?, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendFile(url: URL, fileInfo: FileInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { func sendFile(url: URL, fileInfo: FileInfo, caption: String?, threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendFileUrlFileInfoCaptionRequestHandleCallsCount += 1 sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleCallsCount += 1
if let sendFileUrlFileInfoCaptionRequestHandleClosure = sendFileUrlFileInfoCaptionRequestHandleClosure { if let sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure = sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure {
return await sendFileUrlFileInfoCaptionRequestHandleClosure(url, fileInfo, caption, requestHandle) return await sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure(url, fileInfo, caption, threadRootEventID, requestHandle)
} else { } else {
return sendFileUrlFileInfoCaptionRequestHandleReturnValue return sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleReturnValue
} }
} }
//MARK: - sendImage //MARK: - sendImage
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount = 0 var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount: Int { var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount return sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount returnValue = sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -16297,27 +16297,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount = newValue sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount = newValue sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} }
} }
} }
} }
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCalled: Bool { var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleCalled: Bool {
return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount > 0 return sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleCallsCount > 0
} }
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>! var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleReturnValue: Result<Void, TimelineProxyError>! { var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue return sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} else { } else {
var returnValue: Result<Void, TimelineProxyError>? = nil var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue returnValue = sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -16325,35 +16325,35 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue = newValue sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue = newValue sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} }
} }
} }
} }
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure: ((URL, URL, ImageInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)? var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure: ((URL, URL, ImageInfo, String?, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, caption: String?, threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount += 1 sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleCallsCount += 1
if let sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure = sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure { if let sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure = sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure {
return await sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure(url, thumbnailURL, imageInfo, caption, requestHandle) return await sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure(url, thumbnailURL, imageInfo, caption, threadRootEventID, requestHandle)
} else { } else {
return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleReturnValue return sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleReturnValue
} }
} }
//MARK: - sendLocation //MARK: - sendLocation
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount = 0 var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount = 0
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount: Int { var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount returnValue = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -16361,29 +16361,29 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount = newValue sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount = newValue sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount = newValue
} }
} }
} }
} }
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCalled: Bool { var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDCalled: Bool {
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount > 0 return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDCallsCount > 0
} }
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedArguments: (body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?)? var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReceivedArguments: (body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?, threadRootEventID: String?)?
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedInvocations: [(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?)] = [] var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReceivedInvocations: [(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?, threadRootEventID: String?)] = []
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue: Result<Void, TimelineProxyError>! var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReturnValue: Result<Void, TimelineProxyError>! { var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReturnValue: Result<Void, TimelineProxyError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue
} else { } else {
var returnValue: Result<Void, TimelineProxyError>? = nil var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue returnValue = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -16391,39 +16391,39 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue = newValue sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue = newValue sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue = newValue
} }
} }
} }
} }
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure: ((String, GeoURI, String?, UInt8?, AssetType?) async -> Result<Void, TimelineProxyError>)? var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDClosure: ((String, GeoURI, String?, UInt8?, AssetType?, String?) async -> Result<Void, TimelineProxyError>)?
func sendLocation(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?) async -> Result<Void, TimelineProxyError> { func sendLocation(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?, threadRootEventID: String?) async -> Result<Void, TimelineProxyError> {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount += 1 sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDCallsCount += 1
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedArguments = (body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType) sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReceivedArguments = (body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType, threadRootEventID: threadRootEventID)
DispatchQueue.main.async { DispatchQueue.main.async {
self.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedInvocations.append((body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType)) self.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReceivedInvocations.append((body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType, threadRootEventID: threadRootEventID))
} }
if let sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure { if let sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDClosure = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDClosure {
return await sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure(body, geoURI, description, zoomLevel, assetType) return await sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDClosure(body, geoURI, description, zoomLevel, assetType, threadRootEventID)
} else { } else {
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReturnValue return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReturnValue
} }
} }
//MARK: - sendVideo //MARK: - sendVideo
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount = 0 var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCallsCount: Int { var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount return sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -16431,27 +16431,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount = newValue sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount = newValue sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} }
} }
} }
} }
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCalled: Bool { var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleCalled: Bool {
return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCallsCount > 0 return sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleCallsCount > 0
} }
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>! var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleReturnValue: Result<Void, TimelineProxyError>! { var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue return sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} else { } else {
var returnValue: Result<Void, TimelineProxyError>? = nil var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -16459,35 +16459,35 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue = newValue sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue = newValue sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} }
} }
} }
} }
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure: ((URL, URL, VideoInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)? var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure: ((URL, URL, VideoInfo, String?, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, caption: String?, threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCallsCount += 1 sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleCallsCount += 1
if let sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure = sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure { if let sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure = sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure {
return await sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure(url, thumbnailURL, videoInfo, caption, requestHandle) return await sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure(url, thumbnailURL, videoInfo, caption, threadRootEventID, requestHandle)
} else { } else {
return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleReturnValue return sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleReturnValue
} }
} }
//MARK: - sendVoiceMessage //MARK: - sendVoiceMessage
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount = 0 var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleCallsCount: Int { var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount return sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount returnValue = sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -16495,27 +16495,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount = newValue sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount = newValue sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} }
} }
} }
} }
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleCalled: Bool { var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCalled: Bool {
return sendVoiceMessageUrlAudioInfoWaveformRequestHandleCallsCount > 0 return sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCallsCount > 0
} }
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>! var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue: Result<Void, TimelineProxyError>! { var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue return sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue
} else { } else {
var returnValue: Result<Void, TimelineProxyError>? = nil var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue returnValue = sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -16523,22 +16523,22 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue = newValue sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue = newValue sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} }
} }
} }
} }
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure: ((URL, AudioInfo, [UInt16], @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)? var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure: ((URL, AudioInfo, [UInt16], String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleCallsCount += 1 sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCallsCount += 1
if let sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure = sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure { if let sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure = sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure {
return await sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure(url, audioInfo, waveform, requestHandle) return await sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure(url, audioInfo, waveform, threadRootEventID, requestHandle)
} else { } else {
return sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue return sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue
} }
} }
//MARK: - sendReadReceipt //MARK: - sendReadReceipt
@@ -16823,15 +16823,15 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
//MARK: - createPoll //MARK: - createPoll
var createPollQuestionAnswersPollKindUnderlyingCallsCount = 0 var createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount = 0
var createPollQuestionAnswersPollKindCallsCount: Int { var createPollQuestionAnswersPollKindThreadRootEventIDCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return createPollQuestionAnswersPollKindUnderlyingCallsCount return createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = createPollQuestionAnswersPollKindUnderlyingCallsCount returnValue = createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -16839,29 +16839,29 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
createPollQuestionAnswersPollKindUnderlyingCallsCount = newValue createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
createPollQuestionAnswersPollKindUnderlyingCallsCount = newValue createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount = newValue
} }
} }
} }
} }
var createPollQuestionAnswersPollKindCalled: Bool { var createPollQuestionAnswersPollKindThreadRootEventIDCalled: Bool {
return createPollQuestionAnswersPollKindCallsCount > 0 return createPollQuestionAnswersPollKindThreadRootEventIDCallsCount > 0
} }
var createPollQuestionAnswersPollKindReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind)? var createPollQuestionAnswersPollKindThreadRootEventIDReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind, threadRootEventID: String?)?
var createPollQuestionAnswersPollKindReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind)] = [] var createPollQuestionAnswersPollKindThreadRootEventIDReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind, threadRootEventID: String?)] = []
var createPollQuestionAnswersPollKindUnderlyingReturnValue: Result<Void, TimelineProxyError>! var createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var createPollQuestionAnswersPollKindReturnValue: Result<Void, TimelineProxyError>! { var createPollQuestionAnswersPollKindThreadRootEventIDReturnValue: Result<Void, TimelineProxyError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return createPollQuestionAnswersPollKindUnderlyingReturnValue return createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue
} else { } else {
var returnValue: Result<Void, TimelineProxyError>? = nil var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = createPollQuestionAnswersPollKindUnderlyingReturnValue returnValue = createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -16869,26 +16869,26 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
createPollQuestionAnswersPollKindUnderlyingReturnValue = newValue createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
createPollQuestionAnswersPollKindUnderlyingReturnValue = newValue createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue = newValue
} }
} }
} }
} }
var createPollQuestionAnswersPollKindClosure: ((String, [String], Poll.Kind) async -> Result<Void, TimelineProxyError>)? var createPollQuestionAnswersPollKindThreadRootEventIDClosure: ((String, [String], Poll.Kind, String?) async -> Result<Void, TimelineProxyError>)?
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> { func createPoll(question: String, answers: [String], pollKind: Poll.Kind, threadRootEventID: String?) async -> Result<Void, TimelineProxyError> {
createPollQuestionAnswersPollKindCallsCount += 1 createPollQuestionAnswersPollKindThreadRootEventIDCallsCount += 1
createPollQuestionAnswersPollKindReceivedArguments = (question: question, answers: answers, pollKind: pollKind) createPollQuestionAnswersPollKindThreadRootEventIDReceivedArguments = (question: question, answers: answers, pollKind: pollKind, threadRootEventID: threadRootEventID)
DispatchQueue.main.async { DispatchQueue.main.async {
self.createPollQuestionAnswersPollKindReceivedInvocations.append((question: question, answers: answers, pollKind: pollKind)) self.createPollQuestionAnswersPollKindThreadRootEventIDReceivedInvocations.append((question: question, answers: answers, pollKind: pollKind, threadRootEventID: threadRootEventID))
} }
if let createPollQuestionAnswersPollKindClosure = createPollQuestionAnswersPollKindClosure { if let createPollQuestionAnswersPollKindThreadRootEventIDClosure = createPollQuestionAnswersPollKindThreadRootEventIDClosure {
return await createPollQuestionAnswersPollKindClosure(question, answers, pollKind) return await createPollQuestionAnswersPollKindThreadRootEventIDClosure(question, answers, pollKind, threadRootEventID)
} else { } else {
return createPollQuestionAnswersPollKindReturnValue return createPollQuestionAnswersPollKindThreadRootEventIDReturnValue
} }
} }
//MARK: - editPoll //MARK: - editPoll
@@ -18819,15 +18819,15 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unchecked Sendabl
} }
//MARK: - sendVoiceMessage //MARK: - sendVoiceMessage
var sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount = 0 var sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount = 0
var sendVoiceMessageInRoomAudioConverterCallsCount: Int { var sendVoiceMessageInRoomAudioConverterThreadRootEventIDCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount return sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount returnValue = sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -18835,29 +18835,29 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unchecked Sendabl
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount = newValue sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount = newValue sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount = newValue
} }
} }
} }
} }
var sendVoiceMessageInRoomAudioConverterCalled: Bool { var sendVoiceMessageInRoomAudioConverterThreadRootEventIDCalled: Bool {
return sendVoiceMessageInRoomAudioConverterCallsCount > 0 return sendVoiceMessageInRoomAudioConverterThreadRootEventIDCallsCount > 0
} }
var sendVoiceMessageInRoomAudioConverterReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol)? var sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol, threadRootEventID: String?)?
var sendVoiceMessageInRoomAudioConverterReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol)] = [] var sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol, threadRootEventID: String?)] = []
var sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue: Result<Void, VoiceMessageRecorderError>! var sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue: Result<Void, VoiceMessageRecorderError>!
var sendVoiceMessageInRoomAudioConverterReturnValue: Result<Void, VoiceMessageRecorderError>! { var sendVoiceMessageInRoomAudioConverterThreadRootEventIDReturnValue: Result<Void, VoiceMessageRecorderError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue return sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue
} else { } else {
var returnValue: Result<Void, VoiceMessageRecorderError>? = nil var returnValue: Result<Void, VoiceMessageRecorderError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue returnValue = sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -18865,26 +18865,26 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unchecked Sendabl
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue = newValue sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue = newValue sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue = newValue
} }
} }
} }
} }
var sendVoiceMessageInRoomAudioConverterClosure: ((JoinedRoomProxyProtocol, AudioConverterProtocol) async -> Result<Void, VoiceMessageRecorderError>)? var sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure: ((JoinedRoomProxyProtocol, AudioConverterProtocol, String?) async -> Result<Void, VoiceMessageRecorderError>)?
func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol) async -> Result<Void, VoiceMessageRecorderError> { func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol, threadRootEventID: String?) async -> Result<Void, VoiceMessageRecorderError> {
sendVoiceMessageInRoomAudioConverterCallsCount += 1 sendVoiceMessageInRoomAudioConverterThreadRootEventIDCallsCount += 1
sendVoiceMessageInRoomAudioConverterReceivedArguments = (roomProxy: roomProxy, audioConverter: audioConverter) sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedArguments = (roomProxy: roomProxy, audioConverter: audioConverter, threadRootEventID: threadRootEventID)
DispatchQueue.main.async { DispatchQueue.main.async {
self.sendVoiceMessageInRoomAudioConverterReceivedInvocations.append((roomProxy: roomProxy, audioConverter: audioConverter)) self.sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedInvocations.append((roomProxy: roomProxy, audioConverter: audioConverter, threadRootEventID: threadRootEventID))
} }
if let sendVoiceMessageInRoomAudioConverterClosure = sendVoiceMessageInRoomAudioConverterClosure { if let sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure = sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure {
return await sendVoiceMessageInRoomAudioConverterClosure(roomProxy, audioConverter) return await sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure(roomProxy, audioConverter, threadRootEventID)
} else { } else {
return sendVoiceMessageInRoomAudioConverterReturnValue return sendVoiceMessageInRoomAudioConverterThreadRootEventIDReturnValue
} }
} }
} }
@@ -14,6 +14,7 @@ struct MediaUploadPreviewScreenCoordinatorParameters {
let mediaUploadingPreprocessor: MediaUploadingPreprocessor let mediaUploadingPreprocessor: MediaUploadingPreprocessor
let title: String? let title: String?
let url: URL let url: URL
let threadRootEventID: String?
let shouldShowCaptionWarning: Bool let shouldShowCaptionWarning: Bool
} }
@@ -36,6 +37,7 @@ final class MediaUploadPreviewScreenCoordinator: CoordinatorProtocol {
mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor, mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor,
title: parameters.title, title: parameters.title,
url: parameters.url, url: parameters.url,
threadRootEventID: parameters.threadRootEventID,
shouldShowCaptionWarning: parameters.shouldShowCaptionWarning) shouldShowCaptionWarning: parameters.shouldShowCaptionWarning)
} }
@@ -16,6 +16,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
private let roomProxy: JoinedRoomProxyProtocol private let roomProxy: JoinedRoomProxyProtocol
private let mediaUploadingPreprocessor: MediaUploadingPreprocessor private let mediaUploadingPreprocessor: MediaUploadingPreprocessor
private let url: URL private let url: URL
private let threadRootEventID: String?
private var processingTask: Task<Result<MediaInfo, MediaUploadingPreprocessorError>, Never> private var processingTask: Task<Result<MediaInfo, MediaUploadingPreprocessorError>, Never>
private var requestHandle: SendAttachmentJoinHandleProtocol? private var requestHandle: SendAttachmentJoinHandleProtocol?
@@ -31,11 +32,13 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
mediaUploadingPreprocessor: MediaUploadingPreprocessor, mediaUploadingPreprocessor: MediaUploadingPreprocessor,
title: String?, title: String?,
url: URL, url: URL,
threadRootEventID: String?,
shouldShowCaptionWarning: Bool) { shouldShowCaptionWarning: Bool) {
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.roomProxy = roomProxy self.roomProxy = roomProxy
self.mediaUploadingPreprocessor = mediaUploadingPreprocessor self.mediaUploadingPreprocessor = mediaUploadingPreprocessor
self.url = url self.url = url
self.threadRootEventID = threadRootEventID
// Start processing the media whilst the user is reviewing it/adding a caption. // Start processing the media whilst the user is reviewing it/adding a caption.
processingTask = Task { await mediaUploadingPreprocessor.processMedia(at: url) } processingTask = Task { await mediaUploadingPreprocessor.processMedia(at: url) }
@@ -57,7 +60,9 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
Task { Task {
switch await processingTask.value { switch await processingTask.value {
case .success(let mediaInfo): case .success(let mediaInfo):
switch await sendAttachment(mediaInfo: mediaInfo, caption: caption) { switch await sendAttachment(mediaInfo: mediaInfo,
caption: caption,
threadRootEventID: threadRootEventID) {
case .success: case .success:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
case .failure(let error): case .failure(let error):
@@ -85,7 +90,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
// MARK: - Private // MARK: - Private
private func sendAttachment(mediaInfo: MediaInfo, caption: String?) async -> Result<Void, TimelineProxyError> { private func sendAttachment(mediaInfo: MediaInfo, caption: String?, threadRootEventID: String?) async -> Result<Void, TimelineProxyError> {
let requestHandle: ((SendAttachmentJoinHandleProtocol) -> Void) = { [weak self] handle in let requestHandle: ((SendAttachmentJoinHandleProtocol) -> Void) = { [weak self] handle in
self?.requestHandle = handle self?.requestHandle = handle
} }
@@ -96,22 +101,26 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
thumbnailURL: thumbnailURL, thumbnailURL: thumbnailURL,
imageInfo: imageInfo, imageInfo: imageInfo,
caption: caption, caption: caption,
threadRootEventID: threadRootEventID,
requestHandle: requestHandle) requestHandle: requestHandle)
case let .video(videoURL, thumbnailURL, videoInfo): case let .video(videoURL, thumbnailURL, videoInfo):
return await roomProxy.timeline.sendVideo(url: videoURL, return await roomProxy.timeline.sendVideo(url: videoURL,
thumbnailURL: thumbnailURL, thumbnailURL: thumbnailURL,
videoInfo: videoInfo, videoInfo: videoInfo,
caption: caption, caption: caption,
threadRootEventID: threadRootEventID,
requestHandle: requestHandle) requestHandle: requestHandle)
case let .audio(audioURL, audioInfo): case let .audio(audioURL, audioInfo):
return await roomProxy.timeline.sendAudio(url: audioURL, return await roomProxy.timeline.sendAudio(url: audioURL,
audioInfo: audioInfo, audioInfo: audioInfo,
caption: caption, caption: caption,
threadRootEventID: threadRootEventID,
requestHandle: requestHandle) requestHandle: requestHandle)
case let .file(fileURL, fileInfo): case let .file(fileURL, fileInfo):
return await roomProxy.timeline.sendFile(url: fileURL, return await roomProxy.timeline.sendFile(url: fileURL,
fileInfo: fileInfo, fileInfo: fileInfo,
caption: caption, caption: caption,
threadRootEventID: threadRootEventID,
requestHandle: requestHandle) requestHandle: requestHandle)
} }
} }
@@ -232,6 +232,7 @@ struct MediaUploadPreviewScreen_Previews: PreviewProvider, TestablePreview {
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
title: "App Icon.png", title: "App Icon.png",
url: snapshotURL, url: snapshotURL,
threadRootEventID: nil,
shouldShowCaptionWarning: true) shouldShowCaptionWarning: true)
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {
@@ -80,6 +80,8 @@ struct ComposerToolbarViewState: BindableState {
var keyCommands: [WysiwygKeyCommand] = [] var keyCommands: [WysiwygKeyCommand] = []
var isInThread: Bool
var bindings: ComposerToolbarViewStateBindings var bindings: ComposerToolbarViewStateBindings
var isUploading: Bool { var isUploading: Bool {
@@ -46,6 +46,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
init(initialText: String? = nil, init(initialText: String? = nil,
roomProxy: JoinedRoomProxyProtocol, roomProxy: JoinedRoomProxyProtocol,
isInThread: Bool = false,
wysiwygViewModel: WysiwygComposerViewModel, wysiwygViewModel: WysiwygComposerViewModel,
completionSuggestionService: CompletionSuggestionServiceProtocol, completionSuggestionService: CompletionSuggestionServiceProtocol,
mediaProvider: MediaProviderProtocol, mediaProvider: MediaProviderProtocol,
@@ -68,6 +69,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
audioRecorderState: .init(), audioRecorderState: .init(),
isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted, isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted,
isLocationSharingEnabled: appSettings.mapTilerConfiguration.isEnabled, isLocationSharingEnabled: appSettings.mapTilerConfiguration.isEnabled,
isInThread: isInThread,
bindings: .init()), bindings: .init()),
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
@@ -40,14 +40,16 @@ struct RoomAttachmentPicker: View {
} }
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerTextFormatting) .accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerTextFormatting)
Button { if !context.viewState.isInThread {
context.send(viewAction: .attach(.poll)) Button {
} label: { context.send(viewAction: .attach(.poll))
Label(L10n.screenRoomAttachmentSourcePoll, icon: \.polls) } label: {
Label(L10n.screenRoomAttachmentSourcePoll, icon: \.polls)
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerPoll)
} }
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerPoll)
if context.viewState.isLocationSharingEnabled { if !context.viewState.isInThread, context.viewState.isLocationSharingEnabled {
Button { Button {
context.send(viewAction: .attach(.location)) context.send(viewAction: .attach(.location))
} label: { } label: {
@@ -98,6 +98,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
parserStyle: .elementX) parserStyle: .elementX)
let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText, let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText,
roomProxy: parameters.roomProxy, roomProxy: parameters.roomProxy,
isInThread: false,
wysiwygViewModel: wysiwygViewModel, wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: parameters.completionSuggestionService, completionSuggestionService: parameters.completionSuggestionService,
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.mediaProvider,
@@ -27,8 +27,8 @@ struct ThreadTimelineScreenCoordinatorParameters {
enum ThreadTimelineScreenCoordinatorAction { enum ThreadTimelineScreenCoordinatorAction {
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String) case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
case presentMediaUploadPicker(MediaPickerScreenSource) case presentMediaUploadPicker(MediaPickerScreenSource, threadRootEventID: String?)
case presentMediaUploadPreviewScreen(URL) case presentMediaUploadPreviewScreen(url: URL, threadRootEventID: String?)
case presentLocationPicker case presentLocationPicker
case presentPollForm(mode: PollFormMode) case presentPollForm(mode: PollFormMode)
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?) case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
@@ -78,6 +78,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
composerViewModel = ComposerToolbarViewModel(initialText: nil, composerViewModel = ComposerToolbarViewModel(initialText: nil,
roomProxy: parameters.roomProxy, roomProxy: parameters.roomProxy,
isInThread: true,
wysiwygViewModel: wysiwygViewModel, wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: parameters.completionSuggestionService, completionSuggestionService: parameters.completionSuggestionService,
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.mediaProvider,
@@ -98,11 +99,11 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
case .displayReportContent(let itemID, let senderID): case .displayReportContent(let itemID, let senderID):
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID)) actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
case .displayCameraPicker: case .displayCameraPicker:
actionsSubject.send(.presentMediaUploadPicker(.camera)) actionsSubject.send(.presentMediaUploadPicker(.camera, threadRootEventID: parameters.timelineController.timelineKind.threadRootEventID))
case .displayMediaPicker: case .displayMediaPicker:
actionsSubject.send(.presentMediaUploadPicker(.photoLibrary)) actionsSubject.send(.presentMediaUploadPicker(.photoLibrary, threadRootEventID: parameters.timelineController.timelineKind.threadRootEventID))
case .displayDocumentPicker: case .displayDocumentPicker:
actionsSubject.send(.presentMediaUploadPicker(.documents)) actionsSubject.send(.presentMediaUploadPicker(.documents, threadRootEventID: parameters.timelineController.timelineKind.threadRootEventID))
case .displayMediaPreview(let mediaPreviewViewModel): case .displayMediaPreview(let mediaPreviewViewModel):
viewModel.displayMediaPreview(mediaPreviewViewModel) viewModel.displayMediaPreview(mediaPreviewViewModel)
case .displayLocationPicker: case .displayLocationPicker:
@@ -110,7 +111,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
case .displayPollForm(let mode): case .displayPollForm(let mode):
actionsSubject.send(.presentPollForm(mode: mode)) actionsSubject.send(.presentPollForm(mode: mode))
case .displayMediaUploadPreviewScreen(let url): case .displayMediaUploadPreviewScreen(let url):
actionsSubject.send(.presentMediaUploadPreviewScreen(url)) actionsSubject.send(.presentMediaUploadPreviewScreen(url: url, threadRootEventID: parameters.timelineController.timelineKind.threadRootEventID))
case .displaySenderDetails(userID: let userID): case .displaySenderDetails(userID: let userID):
actionsSubject.send(.presentRoomMemberDetails(userID: userID)) actionsSubject.send(.presentRoomMemberDetails(userID: userID))
case .displayMessageForwarding(let forwardingItem): case .displayMessageForwarding(let forwardingItem):
@@ -362,7 +362,9 @@ class TimelineInteractionHandler {
actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState, waveform: .url(recordingURL), isUploading: true)))) actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState, waveform: .url(recordingURL), isUploading: true))))
await voiceMessageRecorder.stopPlayback() await voiceMessageRecorder.stopPlayback()
switch await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: AudioConverter()) { switch await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: AudioConverter(),
threadRootEventID: timelineController.timelineKind.threadRootEventID) {
case .success: case .success:
await deleteCurrentVoiceMessage() await deleteCurrentVoiceMessage()
case .failure(let error): case .failure(let error):
@@ -229,15 +229,22 @@ final class TimelineProxy: TimelineProxyProtocol {
func sendAudio(url: URL, func sendAudio(url: URL,
audioInfo: AudioInfo, audioInfo: AudioInfo,
caption: String?, caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending audio") MXLog.info("Sending audio")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do { do {
let handle = try timeline.sendAudio(params: .init(source: .file(filename: url.path(percentEncoded: false)), let handle = try timeline.sendAudio(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: caption, caption: caption,
formattedCaption: nil, // Rust will build this from the caption's markdown. formattedCaption: nil, // Rust will build this from the caption's markdown.
mentions: nil, mentions: nil,
replyParams: nil, replyParams: replyParameters,
useSendQueue: true), useSendQueue: true),
audioInfo: audioInfo, audioInfo: audioInfo,
progressWatcher: nil) progressWatcher: nil)
@@ -257,15 +264,22 @@ final class TimelineProxy: TimelineProxyProtocol {
func sendFile(url: URL, func sendFile(url: URL,
fileInfo: FileInfo, fileInfo: FileInfo,
caption: String?, caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending file") MXLog.info("Sending file")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do { do {
let handle = try timeline.sendFile(params: .init(source: .file(filename: url.path(percentEncoded: false)), let handle = try timeline.sendFile(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: caption, caption: caption,
formattedCaption: nil, // Rust will build this from the caption's markdown. formattedCaption: nil, // Rust will build this from the caption's markdown.
mentions: nil, mentions: nil,
replyParams: nil, replyParams: replyParameters,
useSendQueue: true), useSendQueue: true),
fileInfo: fileInfo, fileInfo: fileInfo,
progressWatcher: nil) progressWatcher: nil)
@@ -286,15 +300,22 @@ final class TimelineProxy: TimelineProxyProtocol {
thumbnailURL: URL, thumbnailURL: URL,
imageInfo: ImageInfo, imageInfo: ImageInfo,
caption: String?, caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending image") MXLog.info("Sending image")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do { do {
let handle = try timeline.sendImage(params: .init(source: .file(filename: url.path(percentEncoded: false)), let handle = try timeline.sendImage(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: caption, caption: caption,
formattedCaption: nil, // Rust will build this from the caption's markdown. formattedCaption: nil, // Rust will build this from the caption's markdown.
mentions: nil, mentions: nil,
replyParams: nil, replyParams: replyParameters,
useSendQueue: true), useSendQueue: true),
thumbnailPath: thumbnailURL.path(percentEncoded: false), thumbnailPath: thumbnailURL.path(percentEncoded: false),
imageInfo: imageInfo, imageInfo: imageInfo,
@@ -316,7 +337,8 @@ final class TimelineProxy: TimelineProxyProtocol {
geoURI: GeoURI, geoURI: GeoURI,
description: String?, description: String?,
zoomLevel: UInt8?, zoomLevel: UInt8?,
assetType: AssetType?) async -> Result<Void, TimelineProxyError> { assetType: AssetType?,
threadRootEventID: String?) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending location") MXLog.info("Sending location")
await timeline.sendLocation(body: body, await timeline.sendLocation(body: body,
@@ -334,15 +356,22 @@ final class TimelineProxy: TimelineProxyProtocol {
thumbnailURL: URL, thumbnailURL: URL,
videoInfo: VideoInfo, videoInfo: VideoInfo,
caption: String?, caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending video") MXLog.info("Sending video")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do { do {
let handle = try timeline.sendVideo(params: .init(source: .file(filename: url.path(percentEncoded: false)), let handle = try timeline.sendVideo(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: caption, caption: caption,
formattedCaption: nil, formattedCaption: nil,
mentions: nil, mentions: nil,
replyParams: nil, replyParams: replyParameters,
useSendQueue: true), useSendQueue: true),
thumbnailPath: thumbnailURL.path(percentEncoded: false), thumbnailPath: thumbnailURL.path(percentEncoded: false),
videoInfo: videoInfo, videoInfo: videoInfo,
@@ -363,15 +392,22 @@ final class TimelineProxy: TimelineProxyProtocol {
func sendVoiceMessage(url: URL, func sendVoiceMessage(url: URL,
audioInfo: AudioInfo, audioInfo: AudioInfo,
waveform: [UInt16], waveform: [UInt16],
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending voice message") MXLog.info("Sending voice message")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do { do {
let handle = try timeline.sendVoiceMessage(params: .init(source: .file(filename: url.path(percentEncoded: false)), let handle = try timeline.sendVoiceMessage(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: nil, caption: nil,
formattedCaption: nil, formattedCaption: nil,
mentions: nil, mentions: nil,
replyParams: nil, replyParams: replyParameters,
useSendQueue: true), useSendQueue: true),
audioInfo: audioInfo, audioInfo: audioInfo,
waveform: waveform, waveform: waveform,
@@ -481,7 +517,9 @@ final class TimelineProxy: TimelineProxyProtocol {
// MARK: - Polls // MARK: - Polls
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> { func createPoll(question: String, answers: [String],
pollKind: Poll.Kind,
threadRootEventID: String? = nil) async -> Result<Void, TimelineProxyError> {
MXLog.info("Creating poll") MXLog.info("Creating poll")
do { do {
@@ -82,34 +82,40 @@ protocol TimelineProxyProtocol {
func sendAudio(url: URL, func sendAudio(url: URL,
audioInfo: AudioInfo, audioInfo: AudioInfo,
caption: String?, caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendFile(url: URL, func sendFile(url: URL,
fileInfo: FileInfo, fileInfo: FileInfo,
caption: String?, caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendImage(url: URL, func sendImage(url: URL,
thumbnailURL: URL, thumbnailURL: URL,
imageInfo: ImageInfo, imageInfo: ImageInfo,
caption: String?, caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendLocation(body: String, func sendLocation(body: String,
geoURI: GeoURI, geoURI: GeoURI,
description: String?, description: String?,
zoomLevel: UInt8?, zoomLevel: UInt8?,
assetType: AssetType?) async -> Result<Void, TimelineProxyError> assetType: AssetType?,
threadRootEventID: String?) async -> Result<Void, TimelineProxyError>
func sendVideo(url: URL, func sendVideo(url: URL,
thumbnailURL: URL, thumbnailURL: URL,
videoInfo: VideoInfo, videoInfo: VideoInfo,
caption: String?, caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendVoiceMessage(url: URL, func sendVoiceMessage(url: URL,
audioInfo: AudioInfo, audioInfo: AudioInfo,
waveform: [UInt16], waveform: [UInt16],
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result<Void, TimelineProxyError> func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result<Void, TimelineProxyError>
@@ -124,7 +130,7 @@ protocol TimelineProxyProtocol {
func toggleReaction(_ reaction: String, to eventID: TimelineItemIdentifier.EventOrTransactionID) async -> Result<Void, TimelineProxyError> func toggleReaction(_ reaction: String, to eventID: TimelineItemIdentifier.EventOrTransactionID) async -> Result<Void, TimelineProxyError>
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> func createPoll(question: String, answers: [String], pollKind: Poll.Kind, threadRootEventID: String?) async -> Result<Void, TimelineProxyError>
func editPoll(original eventID: String, func editPoll(original eventID: String,
question: String, question: String,
@@ -144,7 +144,9 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
return .success(waveformData) return .success(waveformData)
} }
func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol) async -> Result<Void, VoiceMessageRecorderError> { func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol,
audioConverter: AudioConverterProtocol,
threadRootEventID: String?) async -> Result<Void, VoiceMessageRecorderError> {
guard let url = audioRecorder.audioFileURL else { guard let url = audioRecorder.audioFileURL else {
return .failure(VoiceMessageRecorderError.missingRecordingFile) return .failure(VoiceMessageRecorderError.missingRecordingFile)
} }
@@ -178,7 +180,8 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
let result = await roomProxy.timeline.sendVoiceMessage(url: oggFile, let result = await roomProxy.timeline.sendVoiceMessage(url: oggFile,
audioInfo: audioInfo, audioInfo: audioInfo,
waveform: waveform) { _ in } waveform: waveform,
threadRootEventID: threadRootEventID) { _ in }
if case .failure(let error) = result { if case .failure(let error) = result {
MXLog.error("Failed to send the voice message. \(error)") MXLog.error("Failed to send the voice message. \(error)")
@@ -38,7 +38,9 @@ protocol VoiceMessageRecorderProtocol {
func seekPlayback(to progress: Double) async func seekPlayback(to progress: Double) async
func deleteRecording() async func deleteRecording() async
func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol) async -> Result<Void, VoiceMessageRecorderError> func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol,
audioConverter: AudioConverterProtocol,
threadRootEventID: String?) async -> Result<Void, VoiceMessageRecorderError>
} }
// sourcery: AutoMockable // sourcery: AutoMockable
@@ -106,16 +106,16 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
private func setUpViewModel(url: URL, expectedCaption: String?) { private func setUpViewModel(url: URL, expectedCaption: String?) {
timelineProxy = TimelineProxyMock(.init()) timelineProxy = TimelineProxyMock(.init())
timelineProxy.sendAudioUrlAudioInfoCaptionRequestHandleClosure = { [weak self] _, _, caption, _ in timelineProxy.sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure = { [weak self] _, _, caption, _, _ in
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
} }
timelineProxy.sendFileUrlFileInfoCaptionRequestHandleClosure = { [weak self] _, _, caption, _ in timelineProxy.sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure = { [weak self] _, _, caption, _, _ in
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
} }
timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure = { [weak self] _, _, _, caption, _, _ in
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
} }
timelineProxy.sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in timelineProxy.sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure = { [weak self] _, _, _, caption, _, _ in
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
} }
@@ -126,6 +126,7 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
title: "Some File", title: "Some File",
url: url, url: url,
threadRootEventID: nil,
shouldShowCaptionWarning: true) shouldShowCaptionWarning: true)
} }
@@ -196,7 +196,9 @@ class VoiceMessageRecorderTests: XCTestCase {
// If there is no recording file, an error is expected // If there is no recording file, an error is expected
audioRecorder.audioFileURL = nil audioRecorder.audioFileURL = nil
guard case .failure(.missingRecordingFile) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { guard case .failure(.missingRecordingFile) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected") XCTFail("An error is expected")
return return
} }
@@ -208,7 +210,9 @@ class VoiceMessageRecorderTests: XCTestCase {
audioConverter.convertToOpusOggSourceURLDestinationURLThrowableError = AudioConverterError.conversionFailed(nil) audioConverter.convertToOpusOggSourceURLDestinationURLThrowableError = AudioConverterError.conversionFailed(nil)
let roomProxy = JoinedRoomProxyMock() let roomProxy = JoinedRoomProxyMock()
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected") XCTFail("An error is expected")
return return
} }
@@ -227,8 +231,10 @@ class VoiceMessageRecorderTests: XCTestCase {
let timelineProxy = TimelineProxyMock() let timelineProxy = TimelineProxyMock()
let roomProxy = JoinedRoomProxyMock() let roomProxy = JoinedRoomProxyMock()
roomProxy.timeline = timelineProxy roomProxy.timeline = timelineProxy
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected") XCTFail("An error is expected")
return return
} }
@@ -248,8 +254,10 @@ class VoiceMessageRecorderTests: XCTestCase {
let timelineProxy = TimelineProxyMock() let timelineProxy = TimelineProxyMock()
let roomProxy = JoinedRoomProxyMock() let roomProxy = JoinedRoomProxyMock()
roomProxy.timeline = timelineProxy roomProxy.timeline = timelineProxy
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected") XCTFail("An error is expected")
return return
} }
@@ -271,8 +279,10 @@ class VoiceMessageRecorderTests: XCTestCase {
let timelineProxy = TimelineProxyMock() let timelineProxy = TimelineProxyMock()
let roomProxy = JoinedRoomProxyMock() let roomProxy = JoinedRoomProxyMock()
roomProxy.timeline = timelineProxy roomProxy.timeline = timelineProxy
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected") XCTFail("An error is expected")
return return
} }
@@ -307,7 +317,7 @@ class VoiceMessageRecorderTests: XCTestCase {
XCTAssertEqual(destination.pathExtension, "ogg") XCTAssertEqual(destination.pathExtension, "ogg")
} }
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure = { url, audioInfo, waveform, _ in timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure = { url, audioInfo, waveform, _, _ in
XCTAssertEqual(url, convertedFileURL) XCTAssertEqual(url, convertedFileURL)
XCTAssertEqual(audioInfo.duration, self.audioRecorder.currentTime) XCTAssertEqual(audioInfo.duration, self.audioRecorder.currentTime)
XCTAssertEqual(audioInfo.size, convertedFileSize) XCTAssertEqual(audioInfo.size, convertedFileSize)
@@ -317,13 +327,13 @@ class VoiceMessageRecorderTests: XCTestCase {
return .success(()) return .success(())
} }
guard case .success = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { guard case .success = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter, threadRootEventID: nil) else {
XCTFail("A success is expected") XCTFail("A success is expected")
return return
} }
XCTAssert(audioConverter.convertToOpusOggSourceURLDestinationURLCalled) XCTAssert(audioConverter.convertToOpusOggSourceURLDestinationURLCalled)
XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleCalled) XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCalled)
// the converted file must have been deleted // the converted file must have been deleted
if let convertedFileURL { if let convertedFileURL {