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