From caf25894d6cdedc24bd64002fb1568eeb1d0d229 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 May 2025 09:07:43 +0200 Subject: [PATCH] Reduce API of JoinedRoom, caller must use the Timeline API from liveTimeline instead. (#4731) This removes lots of boilerplate code. --- .../impl/send/SendLocationPresenter.kt | 4 +- .../impl/send/SendLocationPresenterTest.kt | 13 +- .../MessageComposerPresenter.kt | 2 +- .../messages/impl/MessagesPresenterTest.kt | 8 +- .../AttachmentsPreviewPresenterTest.kt | 64 +++-- .../MessageComposerPresenterTest.kt | 18 +- .../VoiceMessageComposerPresenterTest.kt | 11 +- .../poll/impl/actions/DefaultEndPollAction.kt | 2 +- .../actions/DefaultSendPollResponseAction.kt | 2 +- .../features/poll/impl/data/PollRepository.kt | 2 +- .../impl/create/CreatePollPresenterTest.kt | 14 +- .../features/share/impl/SharePresenter.kt | 2 +- .../features/share/impl/SharePresenterTest.kt | 9 +- .../libraries/matrix/api/room/JoinedRoom.kt | 134 ----------- .../matrix/impl/room/JoinedRustRoom.kt | 164 ------------- .../matrix/test/room/FakeJoinedRoom.kt | 202 +--------------- .../matrix/test/timeline/FakeTimeline.kt | 222 +++++++++++------- .../libraries/mediaupload/api/MediaSender.kt | 9 +- .../mediaupload/api/MediaSenderTest.kt | 25 +- ...otificationBroadcastReceiverHandlerTest.kt | 2 +- 20 files changed, 258 insertions(+), 651 deletions(-) diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt index ce2b06a8b2..f92b058786 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt @@ -101,7 +101,7 @@ class SendLocationPresenter @Inject constructor( when (mode) { SendLocationState.Mode.PinLocation -> { val geoUri = event.cameraPosition.toGeoUri() - room.sendLocation( + room.liveTimeline.sendLocation( body = generateBody(geoUri), geoUri = geoUri, description = null, @@ -119,7 +119,7 @@ class SendLocationPresenter @Inject constructor( } SendLocationState.Mode.SenderLocation -> { val geoUri = event.toGeoUri() - room.sendLocation( + room.liveTimeline.sendLocation( body = generateBody(geoUri), geoUri = geoUri, description = null, diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt index 0c4bb5e067..267a359608 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTran import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule @@ -266,7 +267,9 @@ class SendLocationPresenterTest { Result.success(Unit) } val joinedRoom = FakeJoinedRoom( - sendLocationResult = sendLocationResult, + liveTimeline = FakeTimeline().apply { + sendLocationLambda = sendLocationResult + }, ) val sendLocationPresenter = createSendLocationPresenter(joinedRoom) fakePermissionsPresenter.givenState( @@ -327,7 +330,9 @@ class SendLocationPresenterTest { Result.success(Unit) } val joinedRoom = FakeJoinedRoom( - sendLocationResult = sendLocationResult, + liveTimeline = FakeTimeline().apply { + sendLocationLambda = sendLocationResult + }, ) val sendLocationPresenter = createSendLocationPresenter(joinedRoom) fakePermissionsPresenter.givenState( @@ -388,7 +393,9 @@ class SendLocationPresenterTest { Result.success(Unit) } val joinedRoom = FakeJoinedRoom( - sendLocationResult = sendLocationResult, + liveTimeline = FakeTimeline().apply { + sendLocationLambda = sendLocationResult + }, ) val sendLocationPresenter = createSendLocationPresenter(joinedRoom) fakePermissionsPresenter.givenState( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 11bb0868e7..dc35fe1642 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -424,7 +424,7 @@ class MessageComposerPresenter @AssistedInject constructor( resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit) when (capturedMode) { is MessageComposerMode.Attachment, - is MessageComposerMode.Normal -> room.sendMessage( + is MessageComposerMode.Normal -> room.liveTimeline.sendMessage( body = message.markdown, htmlBody = message.html, intentionalMentions = message.intentionalMentions diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 2a3c34feee..3cc00d0759 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -5,6 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.messages.impl import androidx.lifecycle.Lifecycle @@ -96,6 +98,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -173,12 +176,14 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId())) + advanceUntilIdle() assert(toggleReactionSuccess) .isCalledOnce() .with(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId())) // No crashes when sending a reaction failed - timeline.apply { toggleReactionLambda = toggleReactionFailure } + timeline.toggleReactionLambda = toggleReactionFailure initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId())) + advanceUntilIdle() assert(toggleReactionFailure) .isCalledOnce() .with(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId())) @@ -209,6 +214,7 @@ class MessagesPresenterTest { val initialState = awaitItem() initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId())) initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId())) + advanceUntilIdle() assert(toggleReactionSuccess) .isCalledExactly(2) .withSequence( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt index 2124d38b56..2dd7bca3c8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.test.A_CAPTION import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo @@ -108,15 +109,18 @@ class AttachmentsPreviewPresenterTest { fun `present - send media success scenario`() = runTest { val sendFileResult = lambdaRecorder> { _, _, _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } + Result.success(FakeMediaUploadHandler()) + } val room = FakeJoinedRoom( - progressCallbackValues = listOf( - Pair(0, 10), - Pair(5, 10), - Pair(10, 10) - ), - sendFileResult = sendFileResult, + liveTimeline = FakeTimeline( + progressCallbackValues = listOf( + Pair(0, 10), + Pair(5, 10), + Pair(10, 10) + ), + ).apply { + sendFileLambda = sendFileResult + }, ) val onDoneListener = lambdaRecorder { } val presenter = createAttachmentsPreviewPresenter( @@ -146,10 +150,12 @@ class AttachmentsPreviewPresenterTest { fun `present - send media after pre-processing success scenario`() = runTest { val sendFileResult = lambdaRecorder> { _, _, _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } + Result.success(FakeMediaUploadHandler()) + } val room = FakeJoinedRoom( - sendFileResult = sendFileResult, + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val onDoneListener = lambdaRecorder { } val processLatch = CompletableDeferred() @@ -182,10 +188,12 @@ class AttachmentsPreviewPresenterTest { fun `present - send media before pre-processing success scenario`() = runTest { val sendFileResult = lambdaRecorder> { _, _, _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } + Result.success(FakeMediaUploadHandler()) + } val room = FakeJoinedRoom( - sendFileResult = sendFileResult, + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val onDoneListener = lambdaRecorder { } val processLatch = CompletableDeferred() @@ -298,7 +306,9 @@ class AttachmentsPreviewPresenterTest { givenImageResult() } val room = FakeJoinedRoom( - sendImageResult = sendImageResult, + liveTimeline = FakeTimeline().apply { + sendImageLambda = sendImageResult + }, ) val onDoneListener = lambdaRecorder { } val presenter = createAttachmentsPreviewPresenter( @@ -340,7 +350,9 @@ class AttachmentsPreviewPresenterTest { givenVideoResult() } val room = FakeJoinedRoom( - sendVideoResult = sendVideoResult, + liveTimeline = FakeTimeline().apply { + sendVideoLambda = sendVideoResult + }, ) val onDoneListener = lambdaRecorder { } val presenter = createAttachmentsPreviewPresenter( @@ -382,7 +394,9 @@ class AttachmentsPreviewPresenterTest { givenAudioResult() } val room = FakeJoinedRoom( - sendAudioResult = sendAudioResult, + liveTimeline = FakeTimeline().apply { + sendAudioLambda = sendAudioResult + }, ) val onDoneListener = lambdaRecorder { } val presenter = createAttachmentsPreviewPresenter( @@ -416,10 +430,12 @@ class AttachmentsPreviewPresenterTest { val failure = MediaPreProcessor.Failure(null) val sendFileResult = lambdaRecorder> { _, _, _, _, _, _ -> - Result.failure(failure) - } + Result.failure(failure) + } val room = FakeJoinedRoom( - sendFileResult = sendFileResult, + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val presenter = createAttachmentsPreviewPresenter(room = room, mediaUploadOnSendQueueEnabled = false) moleculeFlow(RecompositionMode.Immediate) { @@ -445,11 +461,13 @@ class AttachmentsPreviewPresenterTest { val failure = MediaPreProcessor.Failure(null) val sendFileResult = lambdaRecorder> { _, _, _, _, _, _ -> - Result.failure(failure) - } + Result.failure(failure) + } val onDoneListenerResult = lambdaRecorder {} val room = FakeJoinedRoom( - sendFileResult = sendFileResult, + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val presenter = createAttachmentsPreviewPresenter(room = room, mediaUploadOnSendQueueEnabled = true, onDoneListener = onDoneListenerResult) moleculeFlow(RecompositionMode.Immediate) { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index 9528b81bf3..9146f6515b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -384,7 +384,9 @@ class MessageComposerPresenterTest { val presenter = createPresenter( coroutineScope = this, room = FakeJoinedRoom( - sendMessageResult = { _, _, _ -> Result.success(Unit) }, + liveTimeline = FakeTimeline().apply { + sendMessageLambda = { _, _, _ -> Result.success(Unit) } + }, typingNoticeResult = { Result.success(Unit) } ), ) @@ -418,7 +420,9 @@ class MessageComposerPresenterTest { coroutineScope = this, isRichTextEditorEnabled = false, room = FakeJoinedRoom( - sendMessageResult = { _, _, _ -> Result.success(Unit) }, + liveTimeline = FakeTimeline().apply { + sendMessageLambda = { _, _, _ -> Result.success(Unit) } + }, typingNoticeResult = { Result.success(Unit) } ), ) @@ -1118,16 +1122,16 @@ class MessageComposerPresenterTest { val editMessageLambda = lambdaRecorder { _: EventOrTransactionId, _: String, _: String?, _: List -> Result.success(Unit) } - val timeline = FakeTimeline().apply { - this.replyMessageLambda = replyMessageLambda - this.editMessageLambda = editMessageLambda - } val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List -> Result.success(Unit) } + val timeline = FakeTimeline().apply { + this.replyMessageLambda = replyMessageLambda + this.editMessageLambda = editMessageLambda + sendMessageLambda = sendMessageResult + } val room = FakeJoinedRoom( liveTimeline = timeline, - sendMessageResult = sendMessageResult, typingNoticeResult = { Result.success(Unit) } ) val presenter = createPresenter(room = room, coroutineScope = this) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index b3938dd229..7d0ceb5527 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor @@ -45,6 +46,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -65,7 +67,9 @@ class VoiceMessageComposerPresenterTest { Result.success(FakeMediaUploadHandler()) } private val joinedRoom = FakeJoinedRoom( - sendVoiceMessageResult = sendVoiceMessageResult + liveTimeline = FakeTimeline().apply { + sendVoiceMessageLambda = sendVoiceMessageResult + }, ) private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() } private val mediaSender = MediaSender(mediaPreProcessor, joinedRoom, InMemorySessionPreferencesStore()) @@ -295,7 +299,6 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState()) - val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isCalledOnce() @@ -317,7 +320,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) skipItems(1) // Sending state - + advanceUntilIdle() // Now reply with a voice message messageComposerContext.composerMode = aReplyMode() awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) @@ -653,7 +656,7 @@ class VoiceMessageComposerPresenterTest { permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(), ): VoiceMessageComposerPresenter { return VoiceMessageComposerPresenter( - this, + backgroundScope, voiceRecorder, analyticsService, mediaSender, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt index 33c2aa589d..ea11ef918e 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt @@ -22,7 +22,7 @@ class DefaultEndPollAction @Inject constructor( private val analyticsService: AnalyticsService, ) : EndPollAction { override suspend fun execute(pollStartId: EventId): Result { - return room.endPoll( + return room.liveTimeline.endPoll( pollStartId = pollStartId, text = "The poll with event id: $pollStartId has ended." ).onSuccess { diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt index cade3e9bef..4f1f29df46 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt @@ -22,7 +22,7 @@ class DefaultSendPollResponseAction @Inject constructor( private val analyticsService: AnalyticsService, ) : SendPollResponseAction { override suspend fun execute(pollStartId: EventId, answerId: String): Result { - return room.sendPollResponse( + return room.liveTimeline.sendPollResponse( pollStartId = pollStartId, answers = listOf(answerId), ).onSuccess { diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt index cde6c569a9..ab711df996 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt @@ -41,7 +41,7 @@ class PollRepository @Inject constructor( pollKind: PollKind, maxSelections: Int, ): Result = when (existingPollId) { - null -> room.createPoll( + null -> room.liveTimeline.createPoll( question = question, answers = answers, maxSelections = maxSelections, diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt index 6493fc30a7..67556886bc 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -121,7 +121,9 @@ class CreatePollPresenterTest { val createPollResult = lambdaRecorder, Int, PollKind, Result> { _, _, _, _ -> Result.success(Unit) } val presenter = createCreatePollPresenter( room = FakeJoinedRoom( - createPollResult = createPollResult + liveTimeline = FakeTimeline().apply { + createPollLambda = createPollResult + }, ), mode = CreatePollMode.NewPoll, ) @@ -169,7 +171,9 @@ class CreatePollPresenterTest { } val presenter = createCreatePollPresenter( room = FakeJoinedRoom( - createPollResult = createPollResult + liveTimeline = FakeTimeline().apply { + createPollLambda = createPollResult + }, ), mode = CreatePollMode.NewPoll, ) @@ -253,12 +257,8 @@ class CreatePollPresenterTest { @Test fun `when edit poll fails, error is tracked`() = runTest { val error = Exception("cause") - val editPollResult = lambdaRecorder { _: EventId, _: String, _: List, _: Int, _: PollKind -> - Result.failure(error) - } val presenter = createCreatePollPresenter( room = FakeJoinedRoom( - editPollResult = editPollResult, liveTimeline = timeline, ), mode = CreatePollMode.EditPoll(pollEventId), @@ -276,7 +276,7 @@ class CreatePollPresenterTest { awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A")) awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save) advanceUntilIdle() // Wait for the coroutine to finish - assert(editPollLambda).isCalledOnce() + editPollLambda.assertions().isCalledOnce() assertThat(fakeAnalyticsService.capturedEvents).isEmpty() assertThat(fakeAnalyticsService.trackedErrors).hasSize(1) assertThat(fakeAnalyticsService.trackedErrors).containsExactly( diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt index d46fb22ef4..08e9bdc1ff 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt @@ -94,7 +94,7 @@ class SharePresenter @AssistedInject constructor( onPlainText = { text -> roomIds .map { roomId -> - matrixClient.getJoinedRoom(roomId)?.sendMessage( + matrixClient.getJoinedRoom(roomId)?.liveTimeline?.sendMessage( body = text, htmlBody = null, intentionalMentions = emptyList(), diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt index 7a49982e2b..3504390f68 100644 --- a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt +++ b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore @@ -91,7 +92,9 @@ class SharePresenterTest { @Test fun `present - send text ok`() = runTest { val joinedRoom = FakeJoinedRoom( - sendMessageResult = { _, _, _ -> Result.success(Unit) }, + liveTimeline = FakeTimeline().apply { + sendMessageLambda = { _, _, _ -> Result.success(Unit) } + }, ) val matrixClient = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, joinedRoom) @@ -122,7 +125,9 @@ class SharePresenterTest { Result.success(FakeMediaUploadHandler()) } val joinedRoom = FakeJoinedRoom( - sendFileResult = sendFileResult, + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val matrixClient = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, joinedRoom) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt index ca62c4b134..4373721b70 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt @@ -9,34 +9,21 @@ package io.element.android.libraries.matrix.api.room import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SendHandle -import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange -import io.element.android.libraries.matrix.api.media.AudioInfo -import io.element.android.libraries.matrix.api.media.FileInfo -import io.element.android.libraries.matrix.api.media.ImageInfo -import io.element.android.libraries.matrix.api.media.MediaUploadHandler -import io.element.android.libraries.matrix.api.media.VideoInfo -import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.knock.KnockRequest -import io.element.android.libraries.matrix.api.room.location.AssetType -import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import java.io.File interface JoinedRoom : BaseRoom { val syncUpdateFlow: StateFlow @@ -63,135 +50,14 @@ interface JoinedRoom : BaseRoom { createTimelineParams: CreateTimelineParams, ): Result - suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List): Result - suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List): Result - suspend fun sendImage( - file: File, - thumbnailFile: File?, - imageInfo: ImageInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result - - suspend fun sendVideo( - file: File, - thumbnailFile: File?, - videoInfo: VideoInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result - - suspend fun sendAudio( - file: File, - audioInfo: AudioInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result - - suspend fun sendFile( - file: File, - fileInfo: FileInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result - - suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result - - /** - * Share a location message in the room. - * - * @param body A human readable textual representation of the location. - * @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`. - * Respectively: latitude, longitude, and (optional) uncertainty. - * @param description Optional description of the location to display to the user. - * @param zoomLevel Optional zoom level to display the map at. - * @param assetType Optional type of the location asset. - * Set to SENDER if sharing own location. Set to PIN if sharing any location. - */ - suspend fun sendLocation( - body: String, - geoUri: String, - description: String? = null, - zoomLevel: Int? = null, - assetType: AssetType? = null, - ): Result - - /** - * Create a poll in the room. - * - * @param question The question to ask. - * @param answers The list of answers. - * @param maxSelections The maximum number of answers that can be selected. - * @param pollKind The kind of poll to create. - */ - suspend fun createPoll( - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result - - /** - * Edit a poll in the room. - * - * @param pollStartId The event ID of the poll start event. - * @param question The question to ask. - * @param answers The list of answers. - * @param maxSelections The maximum number of answers that can be selected. - * @param pollKind The kind of poll to create. - */ - suspend fun editPoll( - pollStartId: EventId, - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result - - /** - * Send a response to a poll. - * - * @param pollStartId The event ID of the poll start event. - * @param answers The list of answer ids to send. - */ - suspend fun sendPollResponse(pollStartId: EventId, answers: List): Result - - /** - * Ends a poll in the room. - * - * @param pollStartId The event ID of the poll start event. - * @param text Fallback text of the poll end event. - */ - suspend fun endPoll(pollStartId: EventId, text: String): Result - /** * Send a typing notification. * @param isTyping True if the user is typing, false otherwise. */ suspend fun typingNotice(isTyping: Boolean): Result - suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result - - suspend fun forwardEvent(eventId: EventId, roomIds: List): Result - - suspend fun cancelSend(transactionId: TransactionId): Result - suspend fun inviteUserById(id: UserId): Result suspend fun updateAvatar(mimeType: String, data: ByteArray): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index e006dcdba6..d01675bb29 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -13,20 +13,11 @@ import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SendHandle -import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange -import io.element.android.libraries.matrix.api.media.AudioInfo -import io.element.android.libraries.matrix.api.media.FileInfo -import io.element.android.libraries.matrix.api.media.ImageInfo -import io.element.android.libraries.matrix.api.media.MediaUploadHandler -import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService -import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.IntentionalMention @@ -35,14 +26,11 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsStat import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.knock.KnockRequest -import io.element.android.libraries.matrix.api.room.location.AssetType -import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.impl.core.RustSendHandle @@ -84,7 +72,6 @@ import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions import org.matrix.rustcomponents.sdk.use import timber.log.Timber import uniffi.matrix_sdk.RoomPowerLevelChanges -import java.io.File import kotlin.coroutines.cancellation.CancellationException import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest @@ -249,10 +236,6 @@ class JoinedRustRoom( } } - override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List): Result { - return liveTimeline.sendMessage(body, htmlBody, intentionalMentions) - } - override suspend fun editMessage( eventId: EventId, body: String, @@ -266,159 +249,12 @@ class JoinedRustRoom( } } - override suspend fun sendImage( - file: File, - thumbnailFile: File?, - imageInfo: ImageInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result { - return liveTimeline.sendImage( - file = file, - thumbnailFile = thumbnailFile, - imageInfo = imageInfo, - caption = caption, - formattedCaption = formattedCaption, - progressCallback = progressCallback, - replyParameters = replyParameters - ) - } - - override suspend fun sendVideo( - file: File, - thumbnailFile: File?, - videoInfo: VideoInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result { - return liveTimeline.sendVideo( - file = file, - thumbnailFile = thumbnailFile, - videoInfo = videoInfo, - caption = caption, - formattedCaption = formattedCaption, - progressCallback = progressCallback, - replyParameters = replyParameters - ) - } - - override suspend fun sendAudio( - file: File, - audioInfo: AudioInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result { - return liveTimeline.sendAudio( - file = file, - audioInfo = audioInfo, - caption = caption, - formattedCaption = formattedCaption, - progressCallback = progressCallback, - replyParameters = replyParameters, - ) - } - - override suspend fun sendFile( - file: File, - fileInfo: FileInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result { - return liveTimeline.sendFile( - file = file, - fileInfo = fileInfo, - caption = caption, - formattedCaption = formattedCaption, - progressCallback = progressCallback, - replyParameters = replyParameters, - ) - } - - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result { - return liveTimeline.sendVoiceMessage( - file = file, - audioInfo = audioInfo, - waveform = waveform, - progressCallback = progressCallback, - replyParameters = replyParameters, - ) - } - - override suspend fun sendLocation( - body: String, - geoUri: String, - description: String?, - zoomLevel: Int?, - assetType: AssetType?, - ): Result { - return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType) - } - - override suspend fun createPoll( - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result { - return liveTimeline.createPoll(question, answers, maxSelections, pollKind) - } - - override suspend fun editPoll( - pollStartId: EventId, - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result { - return liveTimeline.editPoll(pollStartId, question, answers, maxSelections, pollKind) - } - - override suspend fun sendPollResponse( - pollStartId: EventId, - answers: List - ): Result { - return liveTimeline.sendPollResponse(pollStartId, answers) - } - - override suspend fun endPoll( - pollStartId: EventId, - text: String - ): Result { - return liveTimeline.endPoll(pollStartId, text) - } - override suspend fun typingNotice(isTyping: Boolean) = withContext(roomDispatcher) { runCatching { innerRoom.typingNotice(isTyping) } } - override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result { - return liveTimeline.toggleReaction(emoji, eventOrTransactionId) - } - - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result { - return liveTimeline.forwardEvent(eventId, roomIds) - } - - override suspend fun cancelSend(transactionId: TransactionId): Result { - return liveTimeline.cancelSend(transactionId) - } - override suspend fun inviteUserById(id: UserId): Result = withContext(roomDispatcher) { runCatching { innerRoom.inviteUserById(id.value) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt index 2cae27b291..64d0a800d6 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt @@ -12,17 +12,9 @@ import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SendHandle -import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange -import io.element.android.libraries.matrix.api.media.AudioInfo -import io.element.android.libraries.matrix.api.media.FileInfo -import io.element.android.libraries.matrix.api.media.ImageInfo -import io.element.android.libraries.matrix.api.media.MediaUploadHandler -import io.element.android.libraries.matrix.api.media.VideoInfo -import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.IntentionalMention @@ -33,16 +25,12 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsStat import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.knock.KnockRequest -import io.element.android.libraries.matrix.api.room.location.AssetType -import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings -import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.tests.testutils.lambda.lambdaError @@ -53,7 +41,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.test.TestScope -import java.io.File class FakeJoinedRoom( val baseRoom: FakeBaseRoom = FakeBaseRoom(), @@ -63,35 +50,16 @@ class FakeJoinedRoom( override val roomTypingMembersFlow: Flow> = MutableStateFlow(emptyList()), override val identityStateChangesFlow: Flow> = MutableStateFlow(emptyList()), override val roomNotificationSettingsStateFlow: StateFlow = - MutableStateFlow(RoomNotificationSettingsState.Unknown), + MutableStateFlow(RoomNotificationSettingsState.Unknown), override val knockRequestsFlow: Flow> = MutableStateFlow(emptyList()), private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private var createTimelineResult: (CreateTimelineParams) -> Result = { lambdaError() }, - private val sendMessageResult: (String, String?, List) -> Result = { _, _, _ -> lambdaError() }, private val editMessageLambda: (EventId, String, String?, List) -> Result = { _, _, _, _ -> lambdaError() }, - private val sendImageResult: (File, File?, ImageInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result = - { _, _, _, _, _, _, _ -> lambdaError() }, - private val sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result = - { _, _, _, _, _, _, _ -> lambdaError() }, - private val sendFileResult: (File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result = - { _, _, _, _, _, _ -> lambdaError() }, - private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result = - { _, _, _, _, _, _ -> lambdaError() }, - private val sendVoiceMessageResult: (File, AudioInfo, List, ProgressCallback?, ReplyParameters?) -> Result = - { _, _, _, _, _ -> lambdaError() }, - private val sendLocationResult: (String, String, String?, Int?, AssetType?) -> Result = { _, _, _, _, _ -> lambdaError() }, private val sendCallNotificationIfNeededResult: () -> Result = { lambdaError() }, private val progressCallbackValues: List> = emptyList(), - private val createPollResult: (String, List, Int, PollKind) -> Result = { _, _, _, _ -> lambdaError() }, - private val editPollResult: (EventId, String, List, Int, PollKind) -> Result = { _, _, _, _, _ -> lambdaError() }, - private val sendPollResponseResult: (EventId, List) -> Result = { _, _ -> lambdaError() }, - private val endPollResult: (EventId, String) -> Result = { _, _ -> lambdaError() }, private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result = { _, _, _, _ -> lambdaError() }, private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result = { lambdaError() }, private val typingNoticeResult: (Boolean) -> Result = { lambdaError() }, - private val toggleReactionResult: (String, EventOrTransactionId) -> Result = { _, _ -> lambdaError() }, - private val forwardEventResult: (EventId, List) -> Result = { _, _ -> lambdaError() }, - private val cancelSendResult: (TransactionId) -> Result = { lambdaError() }, private val inviteUserResult: (UserId) -> Result = { lambdaError() }, private val setNameResult: (String) -> Result = { lambdaError() }, private val setTopicResult: (String) -> Result = { lambdaError() }, @@ -127,10 +95,6 @@ class FakeJoinedRoom( createTimelineResult(createTimelineParams) } - override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List): Result = simulateLongTask { - sendMessageResult(body, htmlBody, intentionalMentions) - } - override suspend fun editMessage( eventId: EventId, body: String, @@ -140,174 +104,10 @@ class FakeJoinedRoom( editMessageLambda(eventId, body, htmlBody, intentionalMentions) } - override suspend fun sendImage( - file: File, - thumbnailFile: File?, - imageInfo: ImageInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendImageResult( - file, - thumbnailFile, - imageInfo, - caption, - formattedCaption, - progressCallback, - replyParameters, - ) - } - - override suspend fun sendVideo( - file: File, - thumbnailFile: File?, - videoInfo: VideoInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendVideoResult( - file, - thumbnailFile, - videoInfo, - caption, - formattedCaption, - progressCallback, - replyParameters, - ) - } - - override suspend fun sendAudio( - file: File, - audioInfo: AudioInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendAudioResult( - file, - audioInfo, - caption, - formattedCaption, - progressCallback, - replyParameters, - ) - } - - override suspend fun sendFile( - file: File, - fileInfo: FileInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendFileResult( - file, - fileInfo, - caption, - formattedCaption, - progressCallback, - replyParameters, - ) - } - - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - replyParameters: ReplyParameters?, - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendVoiceMessageResult( - file, - audioInfo, - waveform, - progressCallback, - replyParameters, - ) - } - - override suspend fun sendLocation( - body: String, - geoUri: String, - description: String?, - zoomLevel: Int?, - assetType: AssetType?, - ): Result = simulateLongTask { - return sendLocationResult( - body, - geoUri, - description, - zoomLevel, - assetType, - ) - } - - override suspend fun createPoll(question: String, answers: List, maxSelections: Int, pollKind: PollKind): Result = simulateLongTask { - return createPollResult( - question, - answers, - maxSelections, - pollKind, - ) - } - - override suspend fun editPoll( - pollStartId: EventId, - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind - ): Result = simulateLongTask { - return editPollResult( - pollStartId, - question, - answers, - maxSelections, - pollKind, - ) - } - - override suspend fun sendPollResponse(pollStartId: EventId, answers: List): Result = simulateLongTask { - return sendPollResponseResult( - pollStartId, - answers, - ) - } - - override suspend fun endPoll(pollStartId: EventId, text: String): Result = simulateLongTask { - endPollResult( - pollStartId, - text, - ) - } - override suspend fun typingNotice(isTyping: Boolean): Result = simulateLongTask { typingNoticeResult(isTyping) } - override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = simulateLongTask { - toggleReactionResult(emoji, eventOrTransactionId) - } - - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = simulateLongTask { - forwardEventResult(eventId, roomIds) - } - - override suspend fun cancelSend(transactionId: TransactionId): Result = simulateLongTask { - cancelSendResult(transactionId) - } - override suspend fun inviteUserById(id: UserId): Result = simulateLongTask { inviteUserResult(id) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index 859f81b5c3..3aa3a2e186 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.test.timeline import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo @@ -26,6 +27,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -47,23 +50,31 @@ class FakeTimeline( ) ), override val membershipChangeEventReceived: Flow = MutableSharedFlow(), + private val progressCallbackValues: List> = emptyList(), + private val cancelSendResult: (TransactionId) -> Result = { lambdaError() }, ) : Timeline { var sendMessageLambda: ( body: String, htmlBody: String?, intentionalMentions: List, ) -> Result = { _, _, _ -> - Result.success(Unit) + lambdaError() + } + + override suspend fun cancelSend(transactionId: TransactionId): Result = simulateLongTask { + cancelSendResult(transactionId) } override suspend fun sendMessage( body: String, htmlBody: String?, intentionalMentions: List, - ): Result = sendMessageLambda(body, htmlBody, intentionalMentions) + ): Result = simulateLongTask { + sendMessageLambda(body, htmlBody, intentionalMentions) + } var redactEventLambda: (eventOrTransactionId: EventOrTransactionId, reason: String?) -> Result = { _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun redactEvent( @@ -77,7 +88,7 @@ class FakeTimeline( htmlBody: String?, intentionalMentions: List, ) -> Result = { _, _, _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun editMessage( @@ -117,7 +128,7 @@ class FakeTimeline( intentionalMentions: List, fromNotification: Boolean, ) -> Result = { _, _, _, _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun replyMessage( @@ -154,15 +165,18 @@ class FakeTimeline( formattedCaption: String?, progressCallback: ProgressCallback?, replyParameters: ReplyParameters?, - ): Result = sendImageLambda( - file, - thumbnailFile, - imageInfo, - caption, - formattedCaption, - progressCallback, - replyParameters, - ) + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendImageLambda( + file, + thumbnailFile, + imageInfo, + caption, + formattedCaption, + progressCallback, + replyParameters, + ) + } var sendVideoLambda: ( file: File, @@ -184,15 +198,18 @@ class FakeTimeline( formattedCaption: String?, progressCallback: ProgressCallback?, replyParameters: ReplyParameters?, - ): Result = sendVideoLambda( - file, - thumbnailFile, - videoInfo, - caption, - formattedCaption, - progressCallback, - replyParameters, - ) + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendVideoLambda( + file, + thumbnailFile, + videoInfo, + caption, + formattedCaption, + progressCallback, + replyParameters, + ) + } var sendAudioLambda: ( file: File, @@ -212,14 +229,17 @@ class FakeTimeline( formattedCaption: String?, progressCallback: ProgressCallback?, replyParameters: ReplyParameters?, - ): Result = sendAudioLambda( - file, - audioInfo, - caption, - formattedCaption, - progressCallback, - replyParameters, - ) + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendAudioLambda( + file, + audioInfo, + caption, + formattedCaption, + progressCallback, + replyParameters, + ) + } var sendFileLambda: ( file: File, @@ -239,14 +259,17 @@ class FakeTimeline( formattedCaption: String?, progressCallback: ProgressCallback?, replyParameters: ReplyParameters?, - ): Result = sendFileLambda( - file, - fileInfo, - caption, - formattedCaption, - progressCallback, - replyParameters, - ) + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendFileLambda( + file, + fileInfo, + caption, + formattedCaption, + progressCallback, + replyParameters, + ) + } var sendVoiceMessageLambda: ( file: File, @@ -264,13 +287,16 @@ class FakeTimeline( waveform: List, progressCallback: ProgressCallback?, replyParameters: ReplyParameters?, - ): Result = sendVoiceMessageLambda( - file, - audioInfo, - waveform, - progressCallback, - replyParameters, - ) + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendVoiceMessageLambda( + file, + audioInfo, + waveform, + progressCallback, + replyParameters, + ) + } var sendLocationLambda: ( body: String, @@ -279,7 +305,7 @@ class FakeTimeline( zoomLevel: Int?, assetType: AssetType?, ) -> Result = { _, _, _, _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun sendLocation( @@ -288,24 +314,30 @@ class FakeTimeline( description: String?, zoomLevel: Int?, assetType: AssetType?, - ): Result = sendLocationLambda( - body, - geoUri, - description, - zoomLevel, - assetType - ) + ): Result = simulateLongTask { + sendLocationLambda( + body, + geoUri, + description, + zoomLevel, + assetType, + ) + } - var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result = { _, _ -> Result.success(Unit) } + var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result = { _, _ -> lambdaError() } - override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = toggleReactionLambda( - emoji, - eventOrTransactionId - ) + override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = simulateLongTask { + toggleReactionLambda( + emoji, + eventOrTransactionId, + ) + } - var forwardEventLambda: (eventId: EventId, roomIds: List) -> Result = { _, _ -> Result.success(Unit) } + var forwardEventLambda: (eventId: EventId, roomIds: List) -> Result = { _, _ -> lambdaError() } - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = forwardEventLambda(eventId, roomIds) + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = simulateLongTask { + forwardEventLambda(eventId, roomIds) + } var createPollLambda: ( question: String, @@ -313,20 +345,17 @@ class FakeTimeline( maxSelections: Int, pollKind: PollKind, ) -> Result = { _, _, _, _ -> - Result.success(Unit) + lambdaError() } - override suspend fun createPoll( - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result = createPollLambda( - question, - answers, - maxSelections, - pollKind - ) + override suspend fun createPoll(question: String, answers: List, maxSelections: Int, pollKind: PollKind): Result = simulateLongTask { + createPollLambda( + question, + answers, + maxSelections, + pollKind, + ) + } var editPollLambda: ( pollStartId: EventId, @@ -335,7 +364,7 @@ class FakeTimeline( maxSelections: Int, pollKind: PollKind, ) -> Result = { _, _, _, _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun editPoll( @@ -343,44 +372,56 @@ class FakeTimeline( question: String, answers: List, maxSelections: Int, - pollKind: PollKind, - ): Result = editPollLambda( - pollStartId, - question, - answers, - maxSelections, - pollKind - ) + pollKind: PollKind + ): Result = simulateLongTask { + editPollLambda( + pollStartId, + question, + answers, + maxSelections, + pollKind, + ) + } var sendPollResponseLambda: ( pollStartId: EventId, answers: List, ) -> Result = { _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun sendPollResponse( pollStartId: EventId, answers: List, - ): Result = sendPollResponseLambda(pollStartId, answers) + ): Result = simulateLongTask { + sendPollResponseLambda( + pollStartId, + answers, + ) + } var endPollLambda: ( pollStartId: EventId, text: String, ) -> Result = { _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun endPoll( pollStartId: EventId, text: String, - ): Result = endPollLambda(pollStartId, text) + ): Result = simulateLongTask { + endPollLambda( + pollStartId, + text, + ) + } var sendReadReceiptLambda: ( eventId: EventId, receiptType: ReceiptType, ) -> Result = { _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun sendReadReceipt( @@ -417,5 +458,12 @@ class FakeTimeline( closeCounter++ } + private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) { + progressCallbackValues.forEach { (current, total) -> + progressCallback?.onProgress(current, total) + delay(1) + } + } + override fun toString() = "FakeTimeline: $name" } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index b9508bcfe7..47451faef4 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.message.ReplyParameters +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job @@ -49,7 +50,7 @@ class MediaSender @Inject constructor( progressCallback: ProgressCallback?, replyParameters: ReplyParameters?, ): Result { - return room.sendMedia( + return room.liveTimeline.sendMedia( uploadInfo = mediaUploadInfo, progressCallback = progressCallback, caption = caption, @@ -76,7 +77,7 @@ class MediaSender @Inject constructor( compressIfPossible = compressIfPossible, ) .flatMapCatching { info -> - room.sendMedia( + room.liveTimeline.sendMedia( uploadInfo = info, progressCallback = progressCallback, caption = caption, @@ -108,7 +109,7 @@ class MediaSender @Inject constructor( audioInfo = audioInfo, waveform = waveForm, ) - room.sendMedia( + room.liveTimeline.sendMedia( uploadInfo = newInfo, progressCallback = progressCallback, caption = null, @@ -130,7 +131,7 @@ class MediaSender @Inject constructor( ongoingUploadJobs.remove(Job) } - private suspend fun JoinedRoom.sendMedia( + private suspend fun Timeline.sendMedia( uploadInfo: MediaUploadInfo, progressCallback: ProgressCallback?, caption: String?, diff --git a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt index 80530c69ab..ae69d85b72 100644 --- a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt +++ b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore @@ -51,7 +52,9 @@ class MediaSenderTest { Result.success(FakeMediaUploadHandler()) } val room = FakeJoinedRoom( - sendImageResult = sendImageResult + liveTimeline = FakeTimeline().apply { + sendImageLambda = sendImageResult + }, ) val sender = createMediaSender(room = room) @@ -74,14 +77,22 @@ class MediaSenderTest { @Test fun `given a failure in the media upload when sending the whole process fails`() = runTest { + val preProcessor = FakeMediaPreProcessor().apply { + givenImageResult() + } val sendImageResult = lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? -> Result.failure(Exception()) } val room = FakeJoinedRoom( - sendImageResult = sendImageResult + liveTimeline = FakeTimeline().apply { + sendImageLambda = sendImageResult + }, + ) + val sender = createMediaSender( + preProcessor = preProcessor, + room = room, ) - val sender = createMediaSender(room = room) val uri = Uri.parse("content://image.jpg") val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg) @@ -94,10 +105,12 @@ class MediaSenderTest { fun `given a cancellation in the media upload when sending the job is cancelled`() = runTest(StandardTestDispatcher()) { val sendFileResult = lambdaRecorder> { _, _, _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } + Result.success(FakeMediaUploadHandler()) + } val room = FakeJoinedRoom( - sendFileResult = sendFileResult + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val sender = createMediaSender(room = room) val sendJob = launch { diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt index dfa440bcf0..8a4f535216 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt @@ -366,7 +366,7 @@ class NotificationBroadcastReceiverHandlerTest { roomId = A_ROOM_ID, ), ) - runCurrent() + advanceUntilIdle() sendMessage.assertions() .isCalledOnce() .with(value(A_MESSAGE), value(null), value(emptyList()))