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 f92b058786..4710e4bd50 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 @@ -26,6 +26,10 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.room.JoinedRoom 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.message.replyInThread +import io.element.android.libraries.matrix.ui.messages.reply.eventId +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.launch import javax.inject.Inject @@ -98,6 +102,18 @@ class SendLocationPresenter @Inject constructor( event: SendLocationEvents.SendLocation, mode: SendLocationState.Mode, ) { + val replyMode = messageComposerContext.composerMode as? MessageComposerMode.Reply + val replyParams = replyMode?.replyToDetails?.let { details -> + if (replyMode.inThread) { + replyInThread(details.eventId()) + } else { + ReplyParameters( + inReplyToEventId = details.eventId(), + enforceThreadReply = false, + replyWithinThread = false + ) + } + } when (mode) { SendLocationState.Mode.PinLocation -> { val geoUri = event.cameraPosition.toGeoUri() @@ -106,7 +122,8 @@ class SendLocationPresenter @Inject constructor( geoUri = geoUri, description = null, zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(), - assetType = AssetType.PIN + assetType = AssetType.PIN, + replyParameters = replyParams, ) analyticsService.capture( Composer( @@ -124,7 +141,8 @@ class SendLocationPresenter @Inject constructor( geoUri = geoUri, description = null, zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(), - assetType = AssetType.SENDER + assetType = AssetType.SENDER, + replyParameters = replyParams, ) analyticsService.capture( Composer( 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 267a359608..7addee38e4 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 @@ -22,6 +22,7 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.matrix.api.room.JoinedRoom 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.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.core.aBuildMeta @@ -263,7 +264,7 @@ class SendLocationPresenterTest { @Test fun `share sender location`() = runTest { - val sendLocationResult = lambdaRecorder> { _, _, _, _, _ -> + val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ -> Result.success(Unit) } val joinedRoom = FakeJoinedRoom( @@ -310,6 +311,7 @@ class SendLocationPresenterTest { value(null), value(15), value(AssetType.SENDER), + value(null), ) assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1) @@ -326,7 +328,7 @@ class SendLocationPresenterTest { @Test fun `share pin location`() = runTest { - val sendLocationResult = lambdaRecorder> { _, _, _, _, _ -> + val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ -> Result.success(Unit) } val joinedRoom = FakeJoinedRoom( @@ -373,6 +375,7 @@ class SendLocationPresenterTest { value(null), value(15), value(AssetType.PIN), + value(null), ) assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1) @@ -389,7 +392,7 @@ class SendLocationPresenterTest { @Test fun `composer context passes through analytics`() = runTest { - val sendLocationResult = lambdaRecorder> { _, _, _, _, _ -> + val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ -> Result.success(Unit) } val joinedRoom = FakeJoinedRoom( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt index d34651f7a4..489e5bd046 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt @@ -8,9 +8,10 @@ package io.element.android.features.messages.impl.draft import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.room.draft.ComposerDraft interface ComposerDraftService { - suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? - suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) + suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?, isVolatile: Boolean): ComposerDraft? + suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?, isVolatile: Boolean) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftStore.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftStore.kt index d1297c1056..c460acb065 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftStore.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftStore.kt @@ -8,9 +8,10 @@ package io.element.android.features.messages.impl.draft import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.room.draft.ComposerDraft interface ComposerDraftStore { - suspend fun loadDraft(roomId: RoomId): ComposerDraft? - suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) + suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?): ComposerDraft? + suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt index 1499aeac1d..57909c5a8a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.draft import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import javax.inject.Inject @@ -18,12 +19,12 @@ class DefaultComposerDraftService @Inject constructor( private val volatileComposerDraftStore: VolatileComposerDraftStore, private val matrixComposerDraftStore: MatrixComposerDraftStore, ) : ComposerDraftService { - override suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? { - return getStore(isVolatile).loadDraft(roomId) + override suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?, isVolatile: Boolean): ComposerDraft? { + return getStore(isVolatile).loadDraft(roomId, threadRoot) } - override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) { - getStore(isVolatile).updateDraft(roomId, draft) + override suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?, isVolatile: Boolean) { + getStore(isVolatile).updateDraft(roomId, threadRoot, draft) } private fun getStore(isVolatile: Boolean): ComposerDraftStore { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt index b850e7b559..88000546dd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.draft import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import timber.log.Timber import javax.inject.Inject @@ -20,26 +21,26 @@ import javax.inject.Inject class MatrixComposerDraftStore @Inject constructor( private val client: MatrixClient, ) : ComposerDraftStore { - override suspend fun loadDraft(roomId: RoomId): ComposerDraft? { + override suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?): ComposerDraft? { return client.getRoom(roomId)?.use { room -> - room.loadComposerDraft() + room.loadComposerDraft(threadRoot) .onFailure { Timber.e(it, "Failed to load composer draft for room $roomId") } .onSuccess { draft -> - room.clearComposerDraft() + room.clearComposerDraft(threadRoot) Timber.d("Loaded composer draft for room $roomId : $draft") } .getOrNull() } } - override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) { + override suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?) { client.getRoom(roomId)?.use { room -> val updateDraftResult = if (draft == null) { - room.clearComposerDraft() + room.clearComposerDraft(threadRoot) } else { - room.saveComposerDraft(draft) + room.saveComposerDraft(draft, threadRoot) } updateDraftResult .onFailure { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt index 568f68a2d6..b7b714f5c9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.draft import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import javax.inject.Inject @@ -17,18 +18,20 @@ import javax.inject.Inject * Currently it's used to store draft message when moving to edit mode. */ class VolatileComposerDraftStore @Inject constructor() : ComposerDraftStore { - private val drafts: MutableMap = mutableMapOf() + private val drafts: MutableMap = mutableMapOf() - override suspend fun loadDraft(roomId: RoomId): ComposerDraft? { + override suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?): ComposerDraft? { + val key = threadRoot?.value ?: roomId.value // Remove the draft from the map when it is loaded - return drafts.remove(roomId) + return drafts.remove(key) } - override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) { + override suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?) { + val key = threadRoot?.value ?: roomId.value if (draft == null) { - drafts.remove(roomId) + drafts.remove(key) } else { - drafts[roomId] = draft + drafts[key] = draft } } } 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 728087fe1c..f7187c5021 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 @@ -219,7 +219,12 @@ class MessageComposerPresenter @AssistedInject constructor( ) LaunchedEffect(Unit) { - val draft = draftService.loadDraft(room.roomId, isVolatile = false) + val draft = draftService.loadDraft( + roomId = room.roomId, + // TODO support threads in composer + threadRoot = null, + isVolatile = false + ) if (draft != null) { applyDraft(draft, markdownTextEditorState, richTextEditorState) } @@ -539,7 +544,9 @@ class MessageComposerPresenter @AssistedInject constructor( draftService.updateDraft( roomId = room.roomId, draft = draft, - isVolatile = isVolatile + isVolatile = isVolatile, + // TODO support threads in composer + threadRoot = null, ) } @@ -700,7 +707,12 @@ class MessageComposerPresenter @AssistedInject constructor( fromEdit: Boolean, ) { // Use the volatile draft only when coming from edit mode otherwise. - val draft = draftService.loadDraft(room.roomId, isVolatile = true).takeIf { fromEdit } + val draft = draftService.loadDraft( + roomId = room.roomId, + // TODO support threads in composer + threadRoot = null, + isVolatile = true + ).takeIf { fromEdit } if (draft != null) { applyDraft(draft, markdownTextEditorState, richTextEditorState) } else { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt index c2d2c3a3de..9d7d4caac5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt @@ -80,6 +80,7 @@ internal fun MessageShield.toText(): String { is MessageShield.UnverifiedIdentity -> CommonStrings.event_shield_reason_unverified_identity is MessageShield.SentInClear -> CommonStrings.event_shield_reason_sent_in_clear is MessageShield.VerificationViolation -> CommonStrings.event_shield_reason_previously_verified + is MessageShield.MismatchedSender -> CommonStrings.event_shield_mismatched_sender } ) } @@ -91,7 +92,8 @@ internal fun MessageShield.toIcon(): ImageVector { is MessageShield.UnknownDevice, is MessageShield.UnsignedDevice, is MessageShield.UnverifiedIdentity, - is MessageShield.VerificationViolation -> CompoundIcons.HelpSolid() + is MessageShield.VerificationViolation, + is MessageShield.MismatchedSender -> CompoundIcons.HelpSolid() is MessageShield.SentInClear -> CompoundIcons.LockOff() } } @@ -122,6 +124,9 @@ internal fun MessageShieldViewPreview() { MessageShieldView( shield = MessageShield.VerificationViolation(false) ) + MessageShieldView( + shield = MessageShield.MismatchedSender(false) + ) } } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt index 6fd4172340..d87b11c951 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt @@ -8,12 +8,22 @@ package io.element.android.features.messages.impl.draft import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.room.draft.ComposerDraft class FakeComposerDraftService : ComposerDraftService { - var loadDraftLambda: (RoomId, Boolean) -> ComposerDraft? = { _, _ -> null } - override suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? = loadDraftLambda(roomId, isVolatile) + var loadDraftLambda: (RoomId, ThreadId?, Boolean) -> ComposerDraft? = { _, _, _ -> null } + override suspend fun loadDraft( + roomId: RoomId, + threadRoot: ThreadId?, + isVolatile: Boolean + ): ComposerDraft? = loadDraftLambda(roomId, threadRoot, isVolatile) - var saveDraftLambda: (RoomId, ComposerDraft?, Boolean) -> Unit = { _, _, _ -> } - override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) = saveDraftLambda(roomId, draft, isVolatile) + var saveDraftLambda: (RoomId, ThreadId?, ComposerDraft?, Boolean) -> Unit = { _, _, _, _ -> } + override suspend fun updateDraft( + roomId: RoomId, + threadRoot: ThreadId?, + draft: ComposerDraft?, + isVolatile: Boolean + ) = saveDraftLambda(roomId, threadRoot, draft, isVolatile) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStoreTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStoreTest.kt index 176037358e..9ec43f3621 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStoreTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStoreTest.kt @@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID import kotlinx.coroutines.test.runTest import org.junit.Test @@ -21,27 +22,51 @@ class VolatileComposerDraftStoreTest { @Test fun `when storing a non-null draft and then loading it, it's loaded and removed`() = runTest { - val initialDraft = sut.loadDraft(roomId) + val initialDraft = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(initialDraft).isNull() - sut.updateDraft(roomId, draft) + sut.updateDraft(roomId = roomId, threadRoot = null, draft = draft) - val loadedDraft = sut.loadDraft(roomId) + val loadedDraft = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(loadedDraft).isEqualTo(draft) - val loadedDraftAfter = sut.loadDraft(roomId) + val loadedDraftAfter = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(loadedDraftAfter).isNull() + + // In thread: + val threadRoot = A_THREAD_ID + val initialThreadDraft = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(initialThreadDraft).isNull() + + sut.updateDraft(roomId = roomId, threadRoot = threadRoot, draft = draft) + + val loadedThreadDraft = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(loadedThreadDraft).isEqualTo(draft) + + val loadedThreadDraftAfter = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(loadedThreadDraftAfter).isNull() } @Test fun `when storing a null draft and then loading it, it's removing the previous one`() = runTest { - val initialDraft = sut.loadDraft(roomId) + val initialDraft = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(initialDraft).isNull() - sut.updateDraft(roomId, draft) - sut.updateDraft(roomId, null) + sut.updateDraft(roomId = roomId, threadRoot = null, draft = draft) + sut.updateDraft(roomId = roomId, threadRoot = null, draft = null) - val loadedDraft = sut.loadDraft(roomId) + val loadedDraft = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(loadedDraft).isNull() + + // In thread: + val threadRoot = A_THREAD_ID + val initialThreadDraft = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(initialThreadDraft).isNull() + + sut.updateDraft(roomId = roomId, threadRoot = threadRoot, draft = draft) + sut.updateDraft(roomId = roomId, threadRoot = threadRoot, draft = null) + + val loadedThreadDraft = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(loadedThreadDraft).isNull() } } 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 f3d815d563..1a7a1d930e 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 @@ -37,6 +37,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder @@ -178,10 +179,10 @@ class MessageComposerPresenterTest { @Test fun `present - change mode to edit`() = runTest { - val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean -> + val loadDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: Boolean -> ComposerDraft(A_MESSAGE, A_MESSAGE, ComposerDraftType.NewMessage) } - val updateDraftLambda = lambdaRecorder { _: RoomId, _: ComposerDraft?, _: Boolean -> } + val updateDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: ComposerDraft?, _: Boolean -> } val draftService = FakeComposerDraftService().apply { this.loadDraftLambda = loadDraftLambda this.saveDraftLambda = updateDraftLambda @@ -207,23 +208,23 @@ class MessageComposerPresenterTest { .isCalledExactly(2) .withSequence( // Automatic load of draft - listOf(value(A_ROOM_ID), value(false)), + listOf(value(A_ROOM_ID), value(null), value(false)), // Load of volatile draft when closing edit mode - listOf(value(A_ROOM_ID), value(true)) + listOf(value(A_ROOM_ID), value(null), value(true)) ) assert(updateDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), any(), value(true)) + .with(value(A_ROOM_ID), value(null), any(), value(true)) } } @Test fun `present - change mode to edit caption`() = runTest { - val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean -> + val loadDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: Boolean -> ComposerDraft(A_MESSAGE, A_MESSAGE, ComposerDraftType.NewMessage) } - val updateDraftLambda = lambdaRecorder { _: RoomId, _: ComposerDraft?, _: Boolean -> } + val updateDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: ComposerDraft?, _: Boolean -> } val draftService = FakeComposerDraftService().apply { this.loadDraftLambda = loadDraftLambda this.saveDraftLambda = updateDraftLambda @@ -249,13 +250,13 @@ class MessageComposerPresenterTest { .isCalledExactly(2) .withSequence( // Automatic load of draft - listOf(value(A_ROOM_ID), value(false)), + listOf(value(A_ROOM_ID), value(null), value(false)), // Load of volatile draft when closing edit mode - listOf(value(A_ROOM_ID), value(true)) + listOf(value(A_ROOM_ID), value(null), value(true)) ) assert(updateDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), any(), value(true)) + .with(value(A_ROOM_ID), value(null), any(), value(true)) } } @@ -303,10 +304,10 @@ class MessageComposerPresenterTest { @Test fun `present - change mode to reply after edit`() = runTest { - val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean -> + val loadDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: Boolean -> ComposerDraft(A_MESSAGE, A_MESSAGE, ComposerDraftType.NewMessage) } - val updateDraftLambda = lambdaRecorder { _: RoomId, _: ComposerDraft?, _: Boolean -> } + val updateDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: ComposerDraft?, _: Boolean -> } val draftService = FakeComposerDraftService().apply { this.loadDraftLambda = loadDraftLambda this.saveDraftLambda = updateDraftLambda @@ -333,11 +334,11 @@ class MessageComposerPresenterTest { assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) assert(updateDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), any(), value(true)) + .with(value(A_ROOM_ID), value(null), any(), value(true)) } } @@ -1246,7 +1247,7 @@ class MessageComposerPresenterTest { @Test fun `present - when there is no draft, nothing is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> null } + val loadDraftLambda = lambdaRecorder { _, _, _ -> null } val composerDraftService = FakeComposerDraftService().apply { this.loadDraftLambda = loadDraftLambda } @@ -1257,7 +1258,7 @@ class MessageComposerPresenterTest { awaitFirstItem() assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) ensureAllEventsConsumed() } @@ -1265,7 +1266,7 @@ class MessageComposerPresenterTest { @Test fun `present - when there is a draft for new message with plain text, it is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> + val loadDraftLambda = lambdaRecorder { _, _, _ -> ComposerDraft(plainText = A_MESSAGE, htmlText = null, draftType = ComposerDraftType.NewMessage) } val composerDraftService = FakeComposerDraftService().apply { @@ -1286,7 +1287,7 @@ class MessageComposerPresenterTest { } assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) ensureAllEventsConsumed() } @@ -1294,7 +1295,7 @@ class MessageComposerPresenterTest { @Test fun `present - when there is a draft for new message with rich text, it is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> + val loadDraftLambda = lambdaRecorder { _, _, _ -> ComposerDraft( plainText = A_MESSAGE, htmlText = A_MESSAGE, @@ -1320,14 +1321,14 @@ class MessageComposerPresenterTest { } assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) ensureAllEventsConsumed() } } @Test fun `present - when there is a draft for edit, it is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> + val loadDraftLambda = lambdaRecorder { _, _, _ -> ComposerDraft( plainText = A_MESSAGE, htmlText = null, @@ -1354,7 +1355,7 @@ class MessageComposerPresenterTest { } assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) ensureAllEventsConsumed() } @@ -1362,7 +1363,7 @@ class MessageComposerPresenterTest { @Test fun `present - when there is a draft for reply, it is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> + val loadDraftLambda = lambdaRecorder { _, _, _ -> ComposerDraft( plainText = A_MESSAGE, htmlText = null, @@ -1400,7 +1401,7 @@ class MessageComposerPresenterTest { } assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) assert(loadReplyDetailsLambda) .isCalledOnce() @@ -1412,7 +1413,7 @@ class MessageComposerPresenterTest { @Test fun `present - when save draft event is invoked and composer is empty then service is called with null draft`() = runTest { - val saveDraftLambda = lambdaRecorder { _, _, _ -> } + val saveDraftLambda = lambdaRecorder { _, _, _, _ -> } val composerDraftService = FakeComposerDraftService().apply { this.saveDraftLambda = saveDraftLambda } @@ -1425,13 +1426,13 @@ class MessageComposerPresenterTest { advanceUntilIdle() assert(saveDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(null), value(false)) + .with(value(A_ROOM_ID), value(null), value(null), value(false)) } } @Test fun `present - when save draft event is invoked and composer is not empty then service is called`() = runTest { - val saveDraftLambda = lambdaRecorder { _, _, _ -> } + val saveDraftLambda = lambdaRecorder { _, _, _, _ -> } val composerDraftService = FakeComposerDraftService().apply { this.saveDraftLambda = saveDraftLambda } @@ -1478,27 +1479,32 @@ class MessageComposerPresenterTest { .withSequence( listOf( value(A_ROOM_ID), + value(null), value(ComposerDraft(plainText = A_MESSAGE, htmlText = null, draftType = ComposerDraftType.NewMessage)), value(false) ), listOf( value(A_ROOM_ID), + value(null), value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.NewMessage)), value(false) ), listOf( value(A_ROOM_ID), + value(null), value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.NewMessage)), // The volatile draft created when switching to edit mode. value(true) ), listOf( value(A_ROOM_ID), + value(null), value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.Edit(AN_EVENT_ID))), value(false) ), listOf( value(A_ROOM_ID), + value(null), // When moving from edit mode, text composer is cleared, so the draft is null value(null), value(false) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 14fbe36141..296f70d939 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -172,7 +172,7 @@ jsoup = "org.jsoup:jsoup:1.20.1" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.6.10" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.6.18" matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index ecbf2af20b..f6152cf0c6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.room import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels @@ -219,17 +220,17 @@ interface BaseRoom : Closeable { /** * Store the given `ComposerDraft` in the state store of this room. */ - suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result + suspend fun saveComposerDraft(composerDraft: ComposerDraft, threadRoot: ThreadId?): Result /** * Retrieve the `ComposerDraft` stored in the state store for this room. */ - suspend fun loadComposerDraft(): Result + suspend fun loadComposerDraft(threadRoot: ThreadId?): Result /** * Clear the `ComposerDraft` stored in the state store for this room. */ - suspend fun clearComposerDraft(): Result + suspend fun clearComposerDraft(threadRoot: ThreadId?): Result /** * Reports a room as inappropriate to the server. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index a940a0981f..00652a4fa9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -131,6 +131,7 @@ interface Timeline : AutoCloseable { * @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. + * @param replyParameters Optional reply parameters to use when sending the location. */ suspend fun sendLocation( body: String, @@ -138,6 +139,7 @@ interface Timeline : AutoCloseable { description: String? = null, zoomLevel: Int? = null, assetType: AssetType? = null, + replyParameters: ReplyParameters?, ): Result suspend fun sendVoiceMessage( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt index 1630f49301..2fd9ec699a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt @@ -28,6 +28,9 @@ sealed interface MessageShield { /** The sender was previously verified but is not anymore. */ data class VerificationViolation(val isCritical: Boolean) : MessageShield + + /** The sender of the event does not match the owner of the device that created the Megolm session. */ + data class MismatchedSender(val isCritical: Boolean) : MessageShield } val MessageShield.isCritical: Boolean @@ -38,4 +41,5 @@ val MessageShield.isCritical: Boolean is MessageShield.UnverifiedIdentity -> isCritical is MessageShield.SentInClear -> isCritical is MessageShield.VerificationViolation -> isCritical + is MessageShield.MismatchedSender -> isCritical } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index 09b7ec1285..b6ff98baed 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -14,6 +14,7 @@ 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.RoomId import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.MessageEventType @@ -266,24 +267,24 @@ class RustBaseRoom( } } - override suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result = withContext(roomDispatcher) { + override suspend fun saveComposerDraft(composerDraft: ComposerDraft, threadRoot: ThreadId?): Result = withContext(roomDispatcher) { runCatchingExceptions { - Timber.d("saveComposerDraft: $composerDraft into $roomId") - innerRoom.saveComposerDraft(composerDraft.into()) + Timber.d("saveComposerDraft: $composerDraft into $roomId for thread root: $threadRoot") + innerRoom.saveComposerDraft(composerDraft.into(), threadRoot = threadRoot?.value) } } - override suspend fun loadComposerDraft(): Result = withContext(roomDispatcher) { + override suspend fun loadComposerDraft(threadRoot: ThreadId?): Result = withContext(roomDispatcher) { runCatchingExceptions { - Timber.d("loadComposerDraft for $roomId") - innerRoom.loadComposerDraft()?.into() + Timber.d("loadComposerDraft for $roomId with thread root: $threadRoot") + innerRoom.loadComposerDraft(threadRoot?.value)?.into() } } - override suspend fun clearComposerDraft(): Result = withContext(roomDispatcher) { + override suspend fun clearComposerDraft(threadRoot: ThreadId?): Result = withContext(roomDispatcher) { runCatchingExceptions { - Timber.d("clearComposerDraft for $roomId") - innerRoom.clearComposerDraft() + Timber.d("clearComposerDraft for $roomId with thread root: $threadRoot") + innerRoom.clearComposerDraft(threadRoot = threadRoot?.value) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index f2285b6aad..2f0154c208 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -485,6 +485,7 @@ class RustTimeline( description: String?, zoomLevel: Int?, assetType: AssetType?, + replyParameters: ReplyParameters?, ): Result = withContext(dispatcher) { runCatchingExceptions { inner.sendLocation( @@ -493,6 +494,7 @@ class RustTimeline( description = description, zoomLevel = zoomLevel?.toUByte(), assetType = assetType?.toInner(), + replyParams = replyParameters?.map(), ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index 6d7ca02784..7cea0e29f4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -176,6 +176,7 @@ private fun ShieldState?.map(): MessageShield? { ShieldStateCode.UNVERIFIED_IDENTITY -> MessageShield.UnverifiedIdentity(isCritical) ShieldStateCode.SENT_IN_CLEAR -> MessageShield.SentInClear(isCritical) ShieldStateCode.VERIFICATION_VIOLATION -> MessageShield.VerificationViolation(isCritical) + ShieldStateCode.MISMATCHED_SENDER -> MessageShield.MismatchedSender(isCritical) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt index c7e1d3e143..c620f5c886 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -11,6 +11,7 @@ import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.MessageEventType @@ -198,11 +199,14 @@ class FakeBaseRoom( return Result.success(Unit) } - override suspend fun saveComposerDraft(composerDraft: ComposerDraft) = saveComposerDraftLambda(composerDraft) + override suspend fun saveComposerDraft( + composerDraft: ComposerDraft, + threadRoot: ThreadId? + ) = saveComposerDraftLambda(composerDraft) - override suspend fun loadComposerDraft() = loadComposerDraftLambda() + override suspend fun loadComposerDraft(threadRoot: ThreadId?) = loadComposerDraftLambda() - override suspend fun clearComposerDraft() = clearComposerDraftLambda() + override suspend fun clearComposerDraft(threadRoot: ThreadId?) = clearComposerDraftLambda() override suspend fun getUpdatedIsEncrypted(): Result = simulateLongTask { Result.success(info().isEncrypted.orFalse()) 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 3aa3a2e186..c4ab0160c4 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 @@ -304,7 +304,8 @@ class FakeTimeline( description: String?, zoomLevel: Int?, assetType: AssetType?, - ) -> Result = { _, _, _, _, _ -> + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _ -> lambdaError() } @@ -314,6 +315,7 @@ class FakeTimeline( description: String?, zoomLevel: Int?, assetType: AssetType?, + replyParameters: ReplyParameters?, ): Result = simulateLongTask { sendLocationLambda( body, @@ -321,6 +323,7 @@ class FakeTimeline( description, zoomLevel, assetType, + replyParameters, ) } diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png index b078dca532..aaeb39225f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c389d128a7f89f8351b31c6043e9b6870af2c71e5e6e7eefbf3c632e6209f8b -size 36218 +oid sha256:dc2234e1f00b0edbe3389464b1ccf375a034bb4f6daca6c80345c85dc2d4a267 +size 44822 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png index 28871cc02f..9c06f766d9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:260b0e20954785563d6abc1a66fbc8823efecf2c8125802fe13b30aa1ab21786 -size 35104 +oid sha256:caa64bc210b30027a5f0eb1ea8c20b3d170b37ee3e1ac8f535233b3209feefc7 +size 43436