diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 55bd3fa4a..310e2dec2 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -19610,15 +19610,15 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unchecked Sendabl } //MARK: - sendVoiceMessage - var sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount = 0 - var sendVoiceMessageInRoomAudioConverterThreadRootEventIDCallsCount: Int { + var sendVoiceMessageTimelineControllerAudioConverterUnderlyingCallsCount = 0 + var sendVoiceMessageTimelineControllerAudioConverterCallsCount: Int { get { if Thread.isMainThread { - return sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount + return sendVoiceMessageTimelineControllerAudioConverterUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount + returnValue = sendVoiceMessageTimelineControllerAudioConverterUnderlyingCallsCount } return returnValue! @@ -19626,29 +19626,29 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unchecked Sendabl } set { if Thread.isMainThread { - sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount = newValue + sendVoiceMessageTimelineControllerAudioConverterUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount = newValue + sendVoiceMessageTimelineControllerAudioConverterUnderlyingCallsCount = newValue } } } } - var sendVoiceMessageInRoomAudioConverterThreadRootEventIDCalled: Bool { - return sendVoiceMessageInRoomAudioConverterThreadRootEventIDCallsCount > 0 + var sendVoiceMessageTimelineControllerAudioConverterCalled: Bool { + return sendVoiceMessageTimelineControllerAudioConverterCallsCount > 0 } - var sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol, threadRootEventID: String?)? - var sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol, threadRootEventID: String?)] = [] + var sendVoiceMessageTimelineControllerAudioConverterReceivedArguments: (timelineController: TimelineControllerProtocol, audioConverter: AudioConverterProtocol)? + var sendVoiceMessageTimelineControllerAudioConverterReceivedInvocations: [(timelineController: TimelineControllerProtocol, audioConverter: AudioConverterProtocol)] = [] - var sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue: Result! - var sendVoiceMessageInRoomAudioConverterThreadRootEventIDReturnValue: Result! { + var sendVoiceMessageTimelineControllerAudioConverterUnderlyingReturnValue: Result! + var sendVoiceMessageTimelineControllerAudioConverterReturnValue: Result! { get { if Thread.isMainThread { - return sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue + return sendVoiceMessageTimelineControllerAudioConverterUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue + returnValue = sendVoiceMessageTimelineControllerAudioConverterUnderlyingReturnValue } return returnValue! @@ -19656,26 +19656,26 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unchecked Sendabl } set { if Thread.isMainThread { - sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue = newValue + sendVoiceMessageTimelineControllerAudioConverterUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue = newValue + sendVoiceMessageTimelineControllerAudioConverterUnderlyingReturnValue = newValue } } } } - var sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure: ((JoinedRoomProxyProtocol, AudioConverterProtocol, String?) async -> Result)? + var sendVoiceMessageTimelineControllerAudioConverterClosure: ((TimelineControllerProtocol, AudioConverterProtocol) async -> Result)? - func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol, threadRootEventID: String?) async -> Result { - sendVoiceMessageInRoomAudioConverterThreadRootEventIDCallsCount += 1 - sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedArguments = (roomProxy: roomProxy, audioConverter: audioConverter, threadRootEventID: threadRootEventID) + func sendVoiceMessage(timelineController: TimelineControllerProtocol, audioConverter: AudioConverterProtocol) async -> Result { + sendVoiceMessageTimelineControllerAudioConverterCallsCount += 1 + sendVoiceMessageTimelineControllerAudioConverterReceivedArguments = (timelineController: timelineController, audioConverter: audioConverter) DispatchQueue.main.async { - self.sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedInvocations.append((roomProxy: roomProxy, audioConverter: audioConverter, threadRootEventID: threadRootEventID)) + self.sendVoiceMessageTimelineControllerAudioConverterReceivedInvocations.append((timelineController: timelineController, audioConverter: audioConverter)) } - if let sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure = sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure { - return await sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure(roomProxy, audioConverter, threadRootEventID) + if let sendVoiceMessageTimelineControllerAudioConverterClosure = sendVoiceMessageTimelineControllerAudioConverterClosure { + return await sendVoiceMessageTimelineControllerAudioConverterClosure(timelineController, audioConverter) } else { - return sendVoiceMessageInRoomAudioConverterThreadRootEventIDReturnValue + return sendVoiceMessageTimelineControllerAudioConverterReturnValue } } } diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index 431e30f34..70a0d6a31 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -362,9 +362,8 @@ 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(), - threadRootEventID: timelineController.timelineKind.threadRootEventID) { + + switch await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController, audioConverter: AudioConverter()) { case .success: await deleteCurrentVoiceMessage() case .failure(let error): diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift index 16ec81992..02cd6e0fd 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift @@ -18,6 +18,8 @@ class MockTimelineController: TimelineControllerProtocol { var incomingItems: [RoomTimelineItemProtocol] = [] var roomProxy: JoinedRoomProxyProtocol? + var timelineProxy: TimelineProxyProtocol? + var roomID: String { roomProxy?.id ?? "MockRoomIdentifier" } var timelineKind: TimelineKind @@ -46,9 +48,13 @@ class MockTimelineController: TimelineControllerProtocol { return mock } - init(timelineKind: TimelineKind = .live, listenForSignals: Bool = false, timelineItems: [RoomTimelineItemProtocol] = RoomTimelineItemFixtures.default) { + init(timelineKind: TimelineKind = .live, + listenForSignals: Bool = false, + timelineItems: [RoomTimelineItemProtocol] = RoomTimelineItemFixtures.default, + timelineProxy: TimelineProxyProtocol? = nil) { self.timelineKind = timelineKind self.timelineItems = timelineItems + self.timelineProxy = timelineProxy callbacks.send(.paginationState(paginationState)) callbacks.send(.isLive(true)) @@ -107,6 +113,10 @@ class MockTimelineController: TimelineControllerProtocol { html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions) async { } + + func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16]) async -> Result { + await timelineProxy?.sendVoiceMessage(url: url, audioInfo: audioInfo, waveform: waveform, requestHandle: { _ in }) ?? .success(()) + } func toggleReaction(_ reaction: String, to eventID: TimelineItemIdentifier.EventOrTransactionID) async { } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index 37fe2192c..5907517c2 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -145,6 +145,8 @@ class TimelineController: TimelineControllerProtocol { func processItemDisappearance(_ itemID: TimelineItemIdentifier) { } + // MARK: - Sending + func sendMessage(_ message: String, html: String?, inReplyToEventID: String?, @@ -153,7 +155,6 @@ class TimelineController: TimelineControllerProtocol { switch await activeTimeline.sendMessage(message, html: html, - threadRootEventID: timelineKind.threadRootEventID, inReplyToEventID: inReplyToEventID, intentionalMentions: intentionalMentions) { case .success: @@ -164,6 +165,12 @@ class TimelineController: TimelineControllerProtocol { } } + func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16]) async -> Result { + await activeTimeline.sendVoiceMessage(url: url, + audioInfo: audioInfo, + waveform: waveform) { _ in } + } + private func donateSendMessageIntent() async { guard let displayName = roomProxy.details.name ?? roomProxy.details.canonicalAlias, !displayName.isEmpty else { MXLog.error("Failed donating send message intent, room missing name or alias.") diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift index beb741b05..9d6277aaf 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift @@ -68,6 +68,8 @@ protocol TimelineControllerProtocol { inReplyToEventID: String?, intentionalMentions: IntentionalMentions) async + func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16]) async -> Result + func edit(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, message: String, html: String?, diff --git a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift index 3cd639678..7b48523c7 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift @@ -144,9 +144,8 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { return .success(waveformData) } - func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, - audioConverter: AudioConverterProtocol, - threadRootEventID: String?) async -> Result { + func sendVoiceMessage(timelineController: TimelineControllerProtocol, + audioConverter: AudioConverterProtocol) async -> Result { guard let url = audioRecorder.audioFileURL else { return .failure(VoiceMessageRecorderError.missingRecordingFile) } @@ -178,9 +177,9 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { return .failure(.failedSendingVoiceMessage) } - let result = await roomProxy.timeline.sendVoiceMessage(url: oggFile, + let result = await timelineController.sendVoiceMessage(url: oggFile, audioInfo: audioInfo, - waveform: waveform) { _ in } + waveform: waveform) 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 c7432342f..645dfee9f 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorderProtocol.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorderProtocol.swift @@ -38,9 +38,8 @@ protocol VoiceMessageRecorderProtocol { func seekPlayback(to progress: Double) async func deleteRecording() async - func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, - audioConverter: AudioConverterProtocol, - threadRootEventID: String?) async -> Result + func sendVoiceMessage(timelineController: TimelineControllerProtocol, + audioConverter: AudioConverterProtocol) async -> Result } // sourcery: AutoMockable diff --git a/UnitTests/Sources/VoiceMessageRecorderTests.swift b/UnitTests/Sources/VoiceMessageRecorderTests.swift index 4279cb23d..efa7eed53 100644 --- a/UnitTests/Sources/VoiceMessageRecorderTests.swift +++ b/UnitTests/Sources/VoiceMessageRecorderTests.swift @@ -192,13 +192,12 @@ class VoiceMessageRecorderTests: XCTestCase { } func testSendVoiceMessage_NoRecordingFile() async throws { - let roomProxy = JoinedRoomProxyMock() - + let timelineController = MockTimelineController() + // If there is no recording file, an error is expected audioRecorder.audioFileURL = nil - guard case .failure(.missingRecordingFile) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, - audioConverter: audioConverter, - threadRootEventID: nil) else { + guard case .failure(.missingRecordingFile) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController, + audioConverter: audioConverter) else { XCTFail("An error is expected") return } @@ -209,10 +208,9 @@ class VoiceMessageRecorderTests: XCTestCase { // If the converter returns an error audioConverter.convertToOpusOggSourceURLDestinationURLThrowableError = AudioConverterError.conversionFailed(nil) - let roomProxy = JoinedRoomProxyMock() - guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, - audioConverter: audioConverter, - threadRootEventID: nil) else { + let timelineController = MockTimelineController() + guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController, + audioConverter: audioConverter) else { XCTFail("An error is expected") return } @@ -229,12 +227,10 @@ class VoiceMessageRecorderTests: XCTestCase { } let timelineProxy = TimelineProxyMock() - let roomProxy = JoinedRoomProxyMock() - roomProxy.timeline = timelineProxy - timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) - guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, - audioConverter: audioConverter, - threadRootEventID: nil) else { + let timelineController = MockTimelineController(timelineProxy: timelineProxy) + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) + guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController, + audioConverter: audioConverter) else { XCTFail("An error is expected") return } @@ -252,12 +248,10 @@ class VoiceMessageRecorderTests: XCTestCase { } let timelineProxy = TimelineProxyMock() - let roomProxy = JoinedRoomProxyMock() - roomProxy.timeline = timelineProxy - timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) - guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, - audioConverter: audioConverter, - threadRootEventID: nil) else { + let timelineController = MockTimelineController(timelineProxy: timelineProxy) + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) + guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController, + audioConverter: audioConverter) else { XCTFail("An error is expected") return } @@ -277,12 +271,10 @@ class VoiceMessageRecorderTests: XCTestCase { // If the media upload fails let timelineProxy = TimelineProxyMock() - let roomProxy = JoinedRoomProxyMock() - roomProxy.timeline = timelineProxy - timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) - guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, - audioConverter: audioConverter, - threadRootEventID: nil) else { + let timelineController = MockTimelineController(timelineProxy: timelineProxy) + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) + guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController, + audioConverter: audioConverter) else { XCTFail("An error is expected") return } @@ -295,8 +287,7 @@ class VoiceMessageRecorderTests: XCTestCase { } let timelineProxy = TimelineProxyMock() - let roomProxy = JoinedRoomProxyMock() - roomProxy.timeline = timelineProxy + let timelineController = MockTimelineController(timelineProxy: timelineProxy) audioRecorder.currentTime = 42 audioRecorder.audioFileURL = imageFileURL _ = await voiceMessageRecorder.startRecording() @@ -317,7 +308,7 @@ class VoiceMessageRecorderTests: XCTestCase { XCTAssertEqual(destination.pathExtension, "ogg") } - timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure = { url, audioInfo, waveform, _, _ in + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure = { url, audioInfo, waveform, _ in XCTAssertEqual(url, convertedFileURL) XCTAssertEqual(audioInfo.duration, self.audioRecorder.currentTime) XCTAssertEqual(audioInfo.size, convertedFileSize) @@ -327,13 +318,13 @@ class VoiceMessageRecorderTests: XCTestCase { return .success(()) } - guard case .success = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter, threadRootEventID: nil) else { + guard case .success = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController, audioConverter: audioConverter) else { XCTFail("A success is expected") return } XCTAssert(audioConverter.convertToOpusOggSourceURLDestinationURLCalled) - XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCalled) + XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleCalled) // the converted file must have been deleted if let convertedFileURL {