From 9981ae7b1e95d34fb0f77a90d56709f945db11cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:21:49 +0200 Subject: [PATCH] fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.7 (#4548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.8 * Fix API breaks: - Add `ReplyParameters` class and parameters to send functions. - Remove outdated OIDC related values. - Stop pre-processing the timeline to add the timeline start item, this is already done by the SDK. * Use the new function to reply to messages in a quick reply from a notification, however: 1. We don't have the thread id value at the moment since the SDK does not provide it yet. 2. The replied to event id wasn't being passed from the notification info. * Remove also timeline start virtual item for DMs, since this wasn't present before either --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín --- .../preview/AttachmentsPreviewPresenter.kt | 6 +- .../MessageComposerPresenter.kt | 15 ++- .../AttachmentsPreviewPresenterTest.kt | 25 +++-- .../MessageComposerPresenterTest.kt | 5 +- .../VoiceMessageComposerPresenterTest.kt | 3 +- .../features/share/impl/SharePresenterTest.kt | 4 +- gradle/libs.versions.toml | 2 +- .../api/notification/NotificationData.kt | 2 + .../libraries/matrix/api/room/MatrixRoom.kt | 62 ++++++------ .../api/room/message/ReplyParameters.kt | 22 +++++ .../libraries/matrix/api/timeline/Timeline.kt | 44 +++++---- .../matrix/impl/RustMatrixClientFactory.kt | 2 +- .../impl/auth/OidcConfigurationProvider.kt | 6 +- .../auth/RustMatrixAuthenticationService.kt | 2 +- .../impl/notification/NotificationMapper.kt | 2 + .../matrix/impl/room/RustMatrixRoom.kt | 82 +++++++++++----- .../impl/room/message/ReplyParameters.kt | 16 ++++ .../matrix/impl/timeline/RustTimeline.kt | 67 ++++++++----- .../item/virtual/VirtualTimelineItemMapper.kt | 1 + .../RoomBeginningPostProcessor.kt | 50 +++++----- .../auth/OidcConfigurationProviderTest.kt | 3 +- .../RustMatrixAuthenticationServiceTest.kt | 2 +- .../fixtures/fakes/FakeRustClientBuilder.kt | 2 +- .../impl/timeline/postprocessor/Fixtures.kt | 4 + .../RoomBeginningPostProcessorTest.kt | 40 +------- .../test/notification/NotificationData.kt | 3 + .../matrix/test/room/FakeMatrixRoom.kt | 95 ++++++++++-------- .../matrix/test/timeline/FakeTimeline.kt | 96 +++++++++++-------- .../libraries/mediaupload/api/MediaSender.kt | 33 +++++-- .../mediaupload/api/MediaSenderTest.kt | 10 +- .../DefaultNotifiableEventResolver.kt | 1 + .../NotificationBroadcastReceiverHandler.kt | 11 ++- .../factories/NotificationCreator.kt | 3 +- .../action/QuickReplyActionFactory.kt | 11 ++- ...otificationBroadcastReceiverHandlerTest.kt | 18 +++- 35 files changed, 454 insertions(+), 296 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/message/ReplyParameters.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/message/ReplyParameters.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index 2f560cd7ac..90bc3d0427 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.api.allFiles @@ -127,6 +128,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( caption = caption, sendActionState = sendActionState, dismissAfterSend = !useSendQueue, + replyParameters = null, ) } } @@ -237,6 +239,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( caption: String?, sendActionState: MutableState, dismissAfterSend: Boolean, + replyParameters: ReplyParameters?, ) = runCatching { val context = coroutineContext val progressCallback = object : ProgressCallback { @@ -251,7 +254,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( mediaUploadInfo = mediaUploadInfo, caption = caption, formattedCaption = null, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ).getOrThrow() }.fold( onSuccess = { 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 95ea275278..32142734e1 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 @@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom 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.api.room.isDm +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails @@ -452,7 +453,19 @@ class MessageComposerPresenter @AssistedInject constructor( } is MessageComposerMode.Reply -> { timelineController.invokeOnCurrentTimeline { - replyMessage(capturedMode.eventId, message.markdown, message.html, message.intentionalMentions) + with(capturedMode) { + replyMessage( + body = message.markdown, + htmlBody = message.html, + intentionalMentions = message.intentionalMentions, + replyParameters = ReplyParameters( + inReplyToEventId = eventId, + enforceThreadReply = inThread, + // This should be false until we add a way to make a reply in a thread an explicit reply to the provided eventId + replyWithinThread = false, + ), + ) + } } } } 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 63a759bd67..d55ea7c7a0 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 @@ -30,6 +30,7 @@ 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 import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.message.ReplyParameters 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 @@ -105,7 +106,8 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media success scenario`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val room = FakeMatrixRoom( @@ -142,7 +144,8 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media after pre-processing success scenario`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val room = FakeMatrixRoom( @@ -177,7 +180,8 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media before pre-processing success scenario`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val room = FakeMatrixRoom( @@ -287,7 +291,7 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send image with caption success scenario`() = runTest { val sendImageResult = - lambdaRecorder> { _, _, _, _, _, _ -> + lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? -> Result.success(FakeMediaUploadHandler()) } val mediaPreProcessor = FakeMediaPreProcessor().apply { @@ -320,6 +324,7 @@ class AttachmentsPreviewPresenterTest { value(A_CAPTION), any(), any(), + any(), ) onDoneListener.assertions().isCalledOnce() } @@ -328,7 +333,7 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send video with caption success scenario`() = runTest { val sendVideoResult = - lambdaRecorder> { _, _, _, _, _, _ -> + lambdaRecorder { _: File, _: File?, _: VideoInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? -> Result.success(FakeMediaUploadHandler()) } val mediaPreProcessor = FakeMediaPreProcessor().apply { @@ -361,6 +366,7 @@ class AttachmentsPreviewPresenterTest { value(A_CAPTION), any(), any(), + any(), ) onDoneListener.assertions().isCalledOnce() } @@ -369,7 +375,7 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send audio with caption success scenario`() = runTest { val sendAudioResult = - lambdaRecorder> { _, _, _, _, _ -> + lambdaRecorder> { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val mediaPreProcessor = FakeMediaPreProcessor().apply { @@ -399,6 +405,7 @@ class AttachmentsPreviewPresenterTest { value(A_CAPTION), any(), any(), + any(), ) onDoneListener.assertions().isCalledOnce() } @@ -407,7 +414,8 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media failure scenario without media queue`() = runTest { val failure = MediaPreProcessor.Failure(null) - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> Result.failure(failure) } val room = FakeMatrixRoom( @@ -435,7 +443,8 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media failure scenario with media queue`() = runTest { val failure = MediaPreProcessor.Failure(null) - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> Result.failure(failure) } val onDoneListenerResult = lambdaRecorder {} 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 2f80c52369..a82df0f2aa 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 @@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState 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.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo @@ -611,7 +612,7 @@ class MessageComposerPresenterTest { @Test fun `present - reply message`() = runTest { - val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List, _: Boolean -> + val replyMessageLambda = lambdaRecorder { _: ReplyParameters, _: String, _: String?, _: List, _: Boolean -> Result.success(Unit) } val timeline = FakeTimeline().apply { @@ -1110,7 +1111,7 @@ class MessageComposerPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - send messages with intentional mentions`() = runTest { - val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List, _: Boolean -> + val replyMessageLambda = lambdaRecorder { _: ReplyParameters, _: String, _: String?, _: List, _: Boolean -> Result.success(Unit) } val editMessageLambda = lambdaRecorder { _: EventOrTransactionId, _: String, _: String?, _: List -> 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 e9ffd00b23..5c3e694062 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 @@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.messagecomposer.aReplyMode import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.matrix.api.core.ProgressCallback 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.FakeMatrixRoom import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer @@ -60,7 +61,7 @@ class VoiceMessageComposerPresenterTest { ) private val analyticsService = FakeAnalyticsService() private val sendVoiceMessageResult = - lambdaRecorder, ProgressCallback?, Result> { _, _, _, _ -> + lambdaRecorder, ProgressCallback?, ReplyParameters?, Result> { _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } private val matrixRoom = FakeMatrixRoom( 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 866170daa3..66cb147158 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 @@ -18,6 +18,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -116,7 +117,8 @@ class SharePresenterTest { @Test fun `present - send media ok`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val matrixRoom = FakeMatrixRoom( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 527c70164f..c5bde3385a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -174,7 +174,7 @@ jsoup = "org.jsoup:jsoup:1.19.1" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.3.24" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.4.8" 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/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 915e1584a9..2c77f70d0a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -9,12 +9,14 @@ package io.element.android.libraries.matrix.api.notification 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.core.UserId import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.MessageType data class NotificationData( val eventId: EventId, + val threadId: ThreadId?, val roomId: RoomId, // mxc url val senderAvatarUrl: String?, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index ec94ec0b92..a2a737a019 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit 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.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility @@ -138,7 +139,8 @@ interface MatrixRoom : Closeable { imageInfo: ImageInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result suspend fun sendVideo( @@ -147,7 +149,8 @@ interface MatrixRoom : Closeable { videoInfo: VideoInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result suspend fun sendAudio( @@ -156,6 +159,7 @@ interface MatrixRoom : Closeable { caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result suspend fun sendFile( @@ -164,8 +168,36 @@ interface MatrixRoom : Closeable { 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 + suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result suspend fun forwardEvent(eventId: EventId, roomIds: List): Result @@ -235,25 +267,6 @@ interface MatrixRoom : Closeable { */ suspend fun clearEventCacheStorage(): 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. * @@ -302,13 +315,6 @@ interface MatrixRoom : Closeable { */ suspend fun endPoll(pollStartId: EventId, text: String): Result - suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback? - ): Result - /** * Send a typing notification. * @param isTyping True if the user is typing, false otherwise. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/message/ReplyParameters.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/message/ReplyParameters.kt new file mode 100644 index 0000000000..6157989a43 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/message/ReplyParameters.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.room.message + +import io.element.android.libraries.matrix.api.core.EventId + +data class ReplyParameters( + val inReplyToEventId: EventId, + val enforceThreadReply: Boolean, + val replyWithinThread: Boolean, +) + +fun replyInThread(eventId: EventId, explicitReply: Boolean = false) = ReplyParameters( + inReplyToEventId = eventId, + enforceThreadReply = true, + replyWithinThread = explicitReply, +) 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 904aae0355..a940a0981f 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 @@ -19,6 +19,7 @@ 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.IntentionalMention 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.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId @@ -75,7 +76,7 @@ interface Timeline : AutoCloseable { ): Result suspend fun replyMessage( - eventId: EventId, + replyParameters: ReplyParameters, body: String, htmlBody: String?, intentionalMentions: List, @@ -88,7 +89,8 @@ interface Timeline : AutoCloseable { imageInfo: ImageInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result suspend fun sendVideo( @@ -97,17 +99,17 @@ interface Timeline : AutoCloseable { videoInfo: VideoInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result - suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result - suspend fun sendAudio( file: File, audioInfo: AudioInfo, caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result suspend fun sendFile( @@ -116,15 +118,9 @@ interface Timeline : AutoCloseable { caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result - suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result - - suspend fun forwardEvent(eventId: EventId, roomIds: List): Result - - suspend fun cancelSend(transactionId: TransactionId): Result = - redactEvent(transactionId.toEventOrTransactionId(), reason = null) - /** * Share a location message in the room. * @@ -144,6 +140,23 @@ interface Timeline : AutoCloseable { assetType: AssetType? = null, ): Result + suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, + ): Result + + suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result + + suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result + + suspend fun forwardEvent(eventId: EventId, roomIds: List): Result + + suspend fun cancelSend(transactionId: TransactionId): Result = + redactEvent(transactionId.toEventOrTransactionId(), reason = null) + /** * Create a poll in the room. * @@ -192,13 +205,6 @@ interface Timeline : AutoCloseable { */ suspend fun endPoll(pollStartId: EventId, text: String): Result - suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback? - ): Result - suspend fun loadReplyDetails(eventId: EventId): InReplyTo /** diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index deca69e1e9..b302784528 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -105,7 +105,7 @@ class RustMatrixClientFactory @Inject constructor( cachePath = sessionPaths.cacheDirectory.absolutePath, ) .setSessionDelegate(sessionDelegate) - .passphrase(passphrase) + .sessionPassphrase(passphrase) .userAgent(userAgentProvider.provide()) .addRootCertificates(userCertificatesProvider.provides()) .autoEnableBackups(true) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProvider.kt index b253985942..ec220112de 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProvider.kt @@ -9,12 +9,9 @@ package io.element.android.libraries.matrix.impl.auth import io.element.android.libraries.matrix.api.auth.OidcConfig import org.matrix.rustcomponents.sdk.OidcConfiguration -import java.io.File import javax.inject.Inject -class OidcConfigurationProvider @Inject constructor( - private val baseDirectory: File, -) { +class OidcConfigurationProvider @Inject constructor() { fun get(): OidcConfiguration = OidcConfiguration( clientName = "Element", redirectUri = OidcConfig.REDIRECT_URI, @@ -29,6 +26,5 @@ class OidcConfigurationProvider @Inject constructor( staticRegistrations = mapOf( "https://id.thirdroom.io/realms/thirdroom" to "elementx", ), - dynamicRegistrationsFile = File(baseDirectory, "oidc/registrations.json").absolutePath, ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 2549fc622a..33af371569 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -324,7 +324,7 @@ class RustMatrixAuthenticationService @Inject constructor( passphrase = pendingPassphrase, slidingSyncType = ClientBuilderSlidingSync.Discovered, ) - .passphrase(passphrase) + .sessionPassphrase(passphrase) .buildWithQrCode(qrCodeData, oidcConfiguration, progressListener) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt index 8b21648071..7fdb4a9fdc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt @@ -36,6 +36,8 @@ class NotificationMapper( ) NotificationData( eventId = eventId, + // FIXME once the `NotificationItem` in the SDK returns the thread id + threadId = null, roomId = roomId, senderAvatarUrl = item.senderInfo.avatarUrl, senderDisplayName = item.senderInfo.displayName, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 5b742e83f8..7ed2ad5eb6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit 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.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.roomNotificationSettings @@ -497,8 +498,17 @@ class RustMatrixRoom( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { - return liveTimeline.sendImage(file, thumbnailFile, imageInfo, caption, formattedCaption, progressCallback) + return liveTimeline.sendImage( + file = file, + thumbnailFile = thumbnailFile, + imageInfo = imageInfo, + caption = caption, + formattedCaption = formattedCaption, + progressCallback = progressCallback, + replyParameters = replyParameters + ) } override suspend fun sendVideo( @@ -508,8 +518,17 @@ class RustMatrixRoom( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { - return liveTimeline.sendVideo(file, thumbnailFile, videoInfo, caption, formattedCaption, progressCallback) + return liveTimeline.sendVideo( + file = file, + thumbnailFile = thumbnailFile, + videoInfo = videoInfo, + caption = caption, + formattedCaption = formattedCaption, + progressCallback = progressCallback, + replyParameters = replyParameters + ) } override suspend fun sendAudio( @@ -518,6 +537,7 @@ class RustMatrixRoom( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { return liveTimeline.sendAudio( file = file, @@ -525,6 +545,7 @@ class RustMatrixRoom( caption = caption, formattedCaption = formattedCaption, progressCallback = progressCallback, + replyParameters = replyParameters, ) } @@ -534,16 +555,44 @@ class RustMatrixRoom( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { return liveTimeline.sendFile( - file, - fileInfo, - caption, - formattedCaption, - progressCallback, + 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 toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result { return liveTimeline.toggleReaction(emoji, eventOrTransactionId) } @@ -631,16 +680,6 @@ class RustMatrixRoom( } } - 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, @@ -674,15 +713,6 @@ class RustMatrixRoom( return liveTimeline.endPoll(pollStartId, text) } - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - ): Result { - return liveTimeline.sendVoiceMessage(file, audioInfo, waveform, progressCallback) - } - override suspend fun typingNotice(isTyping: Boolean) = withContext(roomDispatcher) { runCatching { innerRoom.typingNotice(isTyping) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/message/ReplyParameters.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/message/ReplyParameters.kt new file mode 100644 index 0000000000..415d493e7d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/message/ReplyParameters.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room.message + +import io.element.android.libraries.matrix.api.room.message.ReplyParameters + +fun ReplyParameters.map() = org.matrix.rustcomponents.sdk.ReplyParameters( + eventId = inReplyToEventId.value, + enforceThread = enforceThreadReply, + replyWithinThread = replyWithinThread, +) 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 efe16f0073..f13b5465fb 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 @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.isDm 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.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline @@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.impl.media.toMSC3246range import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.location.toInner +import io.element.android.libraries.matrix.impl.room.message.map import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper @@ -328,7 +330,7 @@ class RustTimeline( } override suspend fun replyMessage( - eventId: EventId, + replyParameters: ReplyParameters, body: String, htmlBody: String?, intentionalMentions: List, @@ -336,7 +338,10 @@ class RustTimeline( ): Result = withContext(dispatcher) { runCatching { val msg = MessageEventContent.from(body, htmlBody, intentionalMentions) - inner.sendReply(msg, eventId.value) + inner.sendReply( + msg = msg, + replyParams = replyParameters.map(), + ) } } @@ -347,6 +352,7 @@ class RustTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOfNotNull(file, thumbnailFile)) { @@ -359,6 +365,7 @@ class RustTimeline( }, useSendQueue = useSendQueue, mentions = null, + replyParams = replyParameters?.map(), ), thumbnailPath = thumbnailFile?.path, imageInfo = imageInfo.map(), @@ -374,6 +381,7 @@ class RustTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOfNotNull(file, thumbnailFile)) { @@ -386,6 +394,7 @@ class RustTimeline( }, useSendQueue = useSendQueue, mentions = null, + replyParams = replyParameters?.map(), ), thumbnailPath = thumbnailFile?.path, videoInfo = videoInfo.map(), @@ -400,6 +409,7 @@ class RustTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOf(file)) { @@ -412,6 +422,7 @@ class RustTimeline( }, useSendQueue = useSendQueue, mentions = null, + replyParams = replyParameters?.map(), ), audioInfo = audioInfo.map(), progressWatcher = progressCallback?.toProgressWatcher() @@ -425,6 +436,7 @@ class RustTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOf(file)) { @@ -437,6 +449,7 @@ class RustTimeline( }, useSendQueue = useSendQueue, mentions = null, + replyParams = replyParameters?.map(), ), fileInfo = fileInfo.map(), progressWatcher = progressCallback?.toProgressWatcher(), @@ -479,6 +492,32 @@ class RustTimeline( } } + override suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, + ): Result { + val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) + return sendAttachment(listOf(file)) { + inner.sendVoiceMessage( + params = UploadParameters( + filename = file.path, + // Maybe allow a caption in the future? + caption = null, + formattedCaption = null, + useSendQueue = useSendQueue, + mentions = null, + replyParams = replyParameters?.map(), + ), + audioInfo = audioInfo.map(), + waveform = waveform.toMSC3246range(), + progressWatcher = progressCallback?.toProgressWatcher(), + ) + } + } + override suspend fun createPoll( question: String, answers: List, @@ -542,30 +581,6 @@ class RustTimeline( } } - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - ): Result { - val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) - return sendAttachment(listOf(file)) { - inner.sendVoiceMessage( - params = UploadParameters( - filename = file.path, - // Maybe allow a caption in the future? - caption = null, - formattedCaption = null, - useSendQueue = useSendQueue, - mentions = null, - ), - audioInfo = audioInfo.map(), - waveform = waveform.toMSC3246range(), - progressWatcher = progressCallback?.toProgressWatcher(), - ) - } - } - private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { return runCatching { MediaUploadHandlerImpl(files, handle()) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt index 8df231bbab..d60d88f168 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt @@ -15,6 +15,7 @@ class VirtualTimelineItemMapper { return when (virtualTimelineItem) { is RustVirtualTimelineItem.DateDivider -> VirtualTimelineItem.DayDivider(virtualTimelineItem.ts.toLong()) RustVirtualTimelineItem.ReadMarker -> VirtualTimelineItem.ReadMarker + RustVirtualTimelineItem.TimelineStart -> VirtualTimelineItem.RoomBeginning } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index cfc03e50b9..4dded4519d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -7,8 +7,6 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor -import androidx.annotation.VisibleForTesting -import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline @@ -32,55 +30,59 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { return when { items.isEmpty() -> items mode == Timeline.Mode.PINNED_EVENTS -> items - isDm -> processForDM(items, roomCreator) + isDm -> processForDM(items, roomCreator, hasMoreToLoadBackwards) hasMoreToLoadBackwards -> items else -> processForRoom(items) } } private fun processForRoom(items: List): List { - val roomBeginningItem = createRoomBeginningItem() - return listOf(roomBeginningItem) + items + // No changes needed, timeline start item is already added by the SDK + return items } - private fun processForDM(items: List, roomCreator: UserId?): List { + private fun processForDM(items: List, roomCreator: UserId?, hasMoreToLoadBackwards: Boolean): List { + val roomBeginningItemIndex = if (!hasMoreToLoadBackwards) { + items.indexOfFirst { it is MatrixTimelineItem.Virtual && it.virtual is VirtualTimelineItem.RoomBeginning }.takeIf { it >= 0 } + } else { + null + } + // Find room creation event. // This is usually the first MatrixTimelineItem.Event (so index 1, index 0 is a date) val roomCreationEventIndex = items.indexOfFirst { val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? StateContent stateEventContent?.content is OtherState.RoomCreate - } + }.takeIf { it >= 0 } // If the parameter roomCreator is null, the creator is the sender of the RoomCreate Event. - val roomCreatorUserId = roomCreator ?: (items.getOrNull(roomCreationEventIndex) as? MatrixTimelineItem.Event)?.event?.sender + val roomCreatorUserId = roomCreator ?: roomCreationEventIndex?.let { + (items.getOrNull(it) as? MatrixTimelineItem.Event)?.event?.sender + } // Find self-join event for the room creator. // This is usually the second MatrixTimelineItem.Event (so index 2) val selfUserJoinedEventIndex = roomCreatorUserId?.let { creatorUserId -> items.indexOfFirst { val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? RoomMembershipContent stateEventContent?.change == MembershipChange.JOINED && stateEventContent.userId == creatorUserId - } - } ?: -1 + }.takeIf { it >= 0 } + } - if (roomCreationEventIndex == -1 && selfUserJoinedEventIndex == -1) { + val indicesToRemove = listOfNotNull( + roomBeginningItemIndex, + roomCreationEventIndex, + selfUserJoinedEventIndex, + ) + if (indicesToRemove.isEmpty()) { + // Nothing to do, return the list as is return items } + // Remove items at the indices we found val newItems = items.toMutableList() - if (selfUserJoinedEventIndex in newItems.indices) { - newItems.removeAt(selfUserJoinedEventIndex) - } - if (roomCreationEventIndex in newItems.indices) { - newItems.removeAt(roomCreationEventIndex) + indicesToRemove.sortedDescending().forEach { index -> + newItems.removeAt(index) } return newItems } - - @VisibleForTesting - fun createRoomBeginningItem(): MatrixTimelineItem.Virtual { - return MatrixTimelineItem.Virtual( - uniqueId = UniqueId("RoomBeginning"), - virtual = VirtualTimelineItem.RoomBeginning - ) - } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt index 0164cfeb20..01321fcdf6 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt @@ -10,12 +10,11 @@ package io.element.android.libraries.matrix.impl.auth import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.auth.OidcConfig import org.junit.Test -import java.io.File class OidcConfigurationProviderTest { @Test fun get() { - val result = OidcConfigurationProvider(File("/base")).get() + val result = OidcConfigurationProvider().get() assertThat(result.redirectUri).isEqualTo(OidcConfig.REDIRECT_URI) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt index 61df8e6455..e2a9d883ee 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt @@ -48,7 +48,7 @@ class RustMatrixAuthenticationServiceTest { sessionStore = sessionStore, rustMatrixClientFactory = rustMatrixClientFactory, passphraseGenerator = FakePassphraseGenerator(), - oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory), + oidcConfigurationProvider = OidcConfigurationProvider(), ) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt index 21c62cf3eb..8852734a71 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt @@ -30,7 +30,7 @@ class FakeRustClientBuilder : ClientBuilder(NoPointer) { override fun roomDecryptionTrustRequirement(trustRequirement: TrustRequirement) = this override fun disableSslVerification() = this override fun homeserverUrl(url: String) = this - override fun passphrase(passphrase: String?) = this + override fun sessionPassphrase(passphrase: String?) = this override fun proxy(url: String) = this override fun requestConfig(config: RequestConfig) = this override fun roomKeyRecipientStrategy(strategy: CollectStrategy) = this diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt index 00e93e0e66..34cb1ac9e1 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt @@ -19,6 +19,10 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +internal val timelineStartEvent = MatrixTimelineItem.Virtual( + uniqueId = UniqueId("timeline_start"), + virtual = VirtualTimelineItem.RoomBeginning, +) internal val roomCreateEvent = MatrixTimelineItem.Event( uniqueId = UniqueId("m.room.create"), event = anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate)) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt index 5d72a68a68..bb1e4581a1 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt @@ -50,8 +50,9 @@ class RoomBeginningPostProcessorTest { } @Test - fun `processor removes room creation event and self-join event from DM timeline`() { + fun `processor removes timeline start, room creation event and self-join event from DM timeline`() { val timelineItems = listOf( + timelineStartEvent, roomCreateEvent, roomCreatorJoinEvent, ) @@ -98,43 +99,6 @@ class RoomBeginningPostProcessorTest { assertThat(processedItems).isEqualTo(expected) } - @Test - fun `processor will add beginning of room item if it's not a DM`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = false, roomCreator = A_USER_ID, hasMoreToLoadBackwards = false) - assertThat(processedItems).isEqualTo( - listOf(processor.createRoomBeginningItem()) + timelineItems - ) - } - - @Test - fun `processor will not add beginning of room item if it's not a DM but the room has more to load`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = false, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true) - assertThat(processedItems).isEqualTo(timelineItems) - } - - @Test - fun `processor will add beginning of room item if it's not a DM, when the parameter roomCreator is null`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = false, roomCreator = null, hasMoreToLoadBackwards = false) - assertThat(processedItems).isEqualTo( - listOf(processor.createRoomBeginningItem()) + timelineItems - ) - } - @Test fun `processor removes items event it's not at the start of the timeline`() { val timelineItems = listOf( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt index f99ff0e355..85473d9367 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.test.notification +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -19,6 +20,7 @@ fun aNotificationData( content: NotificationContent = NotificationContent.MessageLike.RoomEncrypted, isDirect: Boolean = false, hasMention: Boolean = false, + threadId: ThreadId? = null, timestamp: Long = A_TIMESTAMP, senderDisplayName: String? = A_USER_NAME_2, senderIsNameAmbiguous: Boolean = false, @@ -26,6 +28,7 @@ fun aNotificationData( ): NotificationData { return NotificationData( eventId = AN_EVENT_ID, + threadId = threadId, roomId = A_ROOM_ID, senderAvatarUrl = null, senderDisplayName = senderDisplayName, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 7d9bbd1bfc..b7d226d499 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit 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.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility @@ -87,16 +88,16 @@ class FakeMatrixRoom( private val canRedactOtherResult: (UserId) -> Result = { lambdaError() }, private val canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, private val canUserSendMessageResult: (UserId, MessageEventType) -> Result = { _, _ -> lambdaError() }, - private val sendImageResult: (File, File?, ImageInfo, String?, String?, ProgressCallback?) -> Result = + 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 sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?) -> Result = + private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result = { _, _, _, _, _, _ -> lambdaError() }, - private val sendFileResult: (File, FileInfo, String?, String?, ProgressCallback?) -> Result = + private val sendVoiceMessageResult: (File, AudioInfo, List, ProgressCallback?, ReplyParameters?) -> Result = { _, _, _, _, _ -> lambdaError() }, - private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?) -> Result = - { _, _, _, _, _ -> lambdaError() }, - private val sendVoiceMessageResult: (File, AudioInfo, List, ProgressCallback?) -> Result = - { _, _, _, _ -> lambdaError() }, private val setNameResult: (String) -> Result = { lambdaError() }, private val setTopicResult: (String) -> Result = { lambdaError() }, private val updateAvatarResult: (String, ByteArray) -> Result = { _, _ -> lambdaError() }, @@ -332,7 +333,8 @@ class FakeMatrixRoom( imageInfo: ImageInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result = simulateLongTask { simulateSendMediaProgress(progressCallback) sendImageResult( @@ -342,6 +344,7 @@ class FakeMatrixRoom( caption, formattedCaption, progressCallback, + replyParameters, ) } @@ -351,7 +354,8 @@ class FakeMatrixRoom( videoInfo: VideoInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result = simulateLongTask { simulateSendMediaProgress(progressCallback) sendVideoResult( @@ -361,6 +365,7 @@ class FakeMatrixRoom( caption, formattedCaption, progressCallback, + replyParameters, ) } @@ -369,7 +374,8 @@ class FakeMatrixRoom( audioInfo: AudioInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result = simulateLongTask { simulateSendMediaProgress(progressCallback) sendAudioResult( @@ -378,6 +384,7 @@ class FakeMatrixRoom( caption, formattedCaption, progressCallback, + replyParameters, ) } @@ -386,7 +393,8 @@ class FakeMatrixRoom( fileInfo: FileInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result = simulateLongTask { simulateSendMediaProgress(progressCallback) sendFileResult( @@ -395,6 +403,40 @@ class FakeMatrixRoom( 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, ) } @@ -464,22 +506,6 @@ class FakeMatrixRoom( return Result.success(Unit) } - 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, @@ -524,21 +550,6 @@ class FakeMatrixRoom( return endPollResult(pollStartId, text) } - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback? - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendVoiceMessageResult( - file, - audioInfo, - waveform, - progressCallback, - ) - } - override suspend fun typingNotice(isTyping: Boolean): Result { return typingNoticeResult(isTyping) } 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 f43c00c63a..859f81b5c3 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 @@ -18,6 +18,7 @@ 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.IntentionalMention 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.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline @@ -110,7 +111,7 @@ class FakeTimeline( ) var replyMessageLambda: ( - eventId: EventId, + replyParameters: ReplyParameters, body: String, htmlBody: String?, intentionalMentions: List, @@ -120,13 +121,13 @@ class FakeTimeline( } override suspend fun replyMessage( - eventId: EventId, + replyParameters: ReplyParameters, body: String, htmlBody: String?, intentionalMentions: List, fromNotification: Boolean, ): Result = replyMessageLambda( - eventId, + replyParameters, body, htmlBody, intentionalMentions, @@ -140,7 +141,8 @@ class FakeTimeline( body: String?, formattedBody: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _, _, _ -> + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } @@ -151,13 +153,15 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result = sendImageLambda( file, thumbnailFile, imageInfo, caption, formattedCaption, - progressCallback + progressCallback, + replyParameters, ) var sendVideoLambda: ( @@ -167,7 +171,8 @@ class FakeTimeline( body: String?, formattedBody: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _, _, _ -> + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } @@ -178,13 +183,15 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result = sendVideoLambda( file, thumbnailFile, videoInfo, caption, formattedCaption, - progressCallback + progressCallback, + replyParameters, ) var sendAudioLambda: ( @@ -193,7 +200,8 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _, _ -> + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } @@ -203,12 +211,14 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result = sendAudioLambda( file, audioInfo, caption, formattedCaption, - progressCallback + progressCallback, + replyParameters, ) var sendFileLambda: ( @@ -217,7 +227,8 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _, _ -> + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } @@ -227,22 +238,39 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result = sendFileLambda( file, fileInfo, caption, formattedCaption, - progressCallback + progressCallback, + replyParameters, ) - var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result = { _, _ -> Result.success(Unit) } - override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = toggleReactionLambda( - emoji, - eventOrTransactionId - ) + var sendVoiceMessageLambda: ( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } - var forwardEventLambda: (eventId: EventId, roomIds: List) -> Result = { _, _ -> Result.success(Unit) } - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = forwardEventLambda(eventId, roomIds) + override suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, + ): Result = sendVoiceMessageLambda( + file, + audioInfo, + waveform, + progressCallback, + replyParameters, + ) var sendLocationLambda: ( body: String, @@ -268,6 +296,17 @@ class FakeTimeline( assetType ) + var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result = { _, _ -> Result.success(Unit) } + + override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = toggleReactionLambda( + emoji, + eventOrTransactionId + ) + + var forwardEventLambda: (eventId: EventId, roomIds: List) -> Result = { _, _ -> Result.success(Unit) } + + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = forwardEventLambda(eventId, roomIds) + var createPollLambda: ( question: String, answers: List, @@ -337,27 +376,6 @@ class FakeTimeline( text: String, ): Result = endPollLambda(pollStartId, text) - var sendVoiceMessageLambda: ( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } - - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - ): Result = sendVoiceMessageLambda( - file, - audioInfo, - waveform, - progressCallback - ) - var sendReadReceiptLambda: ( eventId: EventId, receiptType: ReceiptType, 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 584539eacc..b68f077df9 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 @@ -12,6 +12,7 @@ import io.element.android.libraries.core.extensions.flatMapCatching 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.MatrixRoom +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job @@ -46,12 +47,14 @@ class MediaSender @Inject constructor( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { return room.sendMedia( uploadInfo = mediaUploadInfo, progressCallback = progressCallback, caption = caption, - formattedCaption = formattedCaption + formattedCaption = formattedCaption, + replyParameters = replyParameters, ) .handleSendResult() } @@ -61,7 +64,8 @@ class MediaSender @Inject constructor( mimeType: String, caption: String? = null, formattedCaption: String? = null, - progressCallback: ProgressCallback? = null + progressCallback: ProgressCallback? = null, + replyParameters: ReplyParameters? = null, ): Result { val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first() return preProcessor @@ -76,7 +80,8 @@ class MediaSender @Inject constructor( uploadInfo = info, progressCallback = progressCallback, caption = caption, - formattedCaption = formattedCaption + formattedCaption = formattedCaption, + replyParameters = replyParameters, ) } .handleSendResult() @@ -86,7 +91,8 @@ class MediaSender @Inject constructor( uri: Uri, mimeType: String, waveForm: List, - progressCallback: ProgressCallback? = null + progressCallback: ProgressCallback? = null, + replyParameters: ReplyParameters? = null, ): Result { return preProcessor .process( @@ -106,7 +112,8 @@ class MediaSender @Inject constructor( uploadInfo = newInfo, progressCallback = progressCallback, caption = null, - formattedCaption = null + formattedCaption = null, + replyParameters = replyParameters, ) } .handleSendResult() @@ -128,6 +135,7 @@ class MediaSender @Inject constructor( progressCallback: ProgressCallback?, caption: String?, formattedCaption: String?, + replyParameters: ReplyParameters?, ): Result { val handler = when (uploadInfo) { is MediaUploadInfo.Image -> { @@ -137,7 +145,8 @@ class MediaSender @Inject constructor( imageInfo = uploadInfo.imageInfo, caption = caption, formattedCaption = formattedCaption, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } is MediaUploadInfo.Video -> { @@ -147,7 +156,8 @@ class MediaSender @Inject constructor( videoInfo = uploadInfo.videoInfo, caption = caption, formattedCaption = formattedCaption, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } is MediaUploadInfo.Audio -> { @@ -156,7 +166,8 @@ class MediaSender @Inject constructor( audioInfo = uploadInfo.audioInfo, caption = caption, formattedCaption = formattedCaption, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } is MediaUploadInfo.VoiceMessage -> { @@ -164,7 +175,8 @@ class MediaSender @Inject constructor( file = uploadInfo.file, audioInfo = uploadInfo.audioInfo, waveform = uploadInfo.waveform, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } is MediaUploadInfo.AnyFile -> { @@ -173,7 +185,8 @@ class MediaSender @Inject constructor( fileInfo = uploadInfo.fileInfo, caption = caption, formattedCaption = formattedCaption, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } } 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 48640da8ea..bd5795af51 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 @@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback 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.room.MatrixRoom +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.FakeMatrixRoom import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor @@ -46,7 +47,7 @@ class MediaSenderTest { @Test fun `given an attachment when sending it the MatrixRoom will call sendMedia`() = runTest { val sendImageResult = - lambdaRecorder> { _, _, _, _, _, _ -> + lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? -> Result.success(FakeMediaUploadHandler()) } val room = FakeMatrixRoom( @@ -74,8 +75,8 @@ class MediaSenderTest { @Test fun `given a failure in the media upload when sending the whole process fails`() = runTest { val sendImageResult = - lambdaRecorder> { _, _, _, _, _, _ -> - Result.failure(Exception()) + lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? -> + Result.failure(Exception()) } val room = FakeMatrixRoom( sendImageResult = sendImageResult @@ -91,7 +92,8 @@ class MediaSenderTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `given a cancellation in the media upload when sending the job is cancelled`() = runTest(StandardTestDispatcher()) { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val room = FakeMatrixRoom( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index c8e8ba4431..b84a13b4f2 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -102,6 +102,7 @@ class DefaultNotifiableEventResolver @Inject constructor( senderId = content.senderId, roomId = roomId, eventId = eventId, + threadId = threadId, noisy = isNoisy, timestamp = this.timestamp, senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt index aea52c8b78..90ed3a3d05 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt @@ -14,9 +14,9 @@ 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.asEventId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.room.message.replyInThread import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory import io.element.android.libraries.push.api.notifications.NotificationCleaner @@ -54,7 +54,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}") when (intent.action) { actionIds.smartReply -> if (roomId != null) { - handleSmartReply(sessionId, roomId, threadId, intent) + handleSmartReply(sessionId, roomId, eventId, threadId, intent) } actionIds.dismissRoom -> if (roomId != null) { notificationCleaner.clearMessagesForRoom(sessionId, roomId) @@ -106,6 +106,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( private fun handleSmartReply( sessionId: SessionId, roomId: RoomId, + replyToEventId: EventId?, threadId: ThreadId?, intent: Intent, ) = appCoroutineScope.launch { @@ -120,6 +121,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( sendMatrixEvent( sessionId = sessionId, roomId = roomId, + replyToEventId = replyToEventId, threadId = threadId, room = room, message = message, @@ -131,6 +133,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( sessionId: SessionId, roomId: RoomId, threadId: ThreadId?, + replyToEventId: EventId?, room: MatrixRoom, message: String, ) { @@ -158,13 +161,13 @@ class NotificationBroadcastReceiverHandler @Inject constructor( ) onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent) - if (threadId != null) { + if (threadId != null && replyToEventId != null) { room.liveTimeline.replyMessage( - eventId = threadId.asEventId(), body = message, htmlBody = null, intentionalMentions = emptyList(), fromNotification = true, + replyParameters = replyInThread(replyToEventId), ) } else { room.liveTimeline.sendMessage( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index f8cf2836b8..d462ebb5f6 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -197,7 +197,8 @@ class DefaultNotificationCreator @Inject constructor( addAction(markAsReadActionFactory.create(roomInfo)) // Quick reply if (!roomInfo.hasSmartReplyError) { - addAction(quickReplyActionFactory.create(roomInfo, threadId)) + val latestEventId = events.lastOrNull()?.eventId + addAction(quickReplyActionFactory.create(roomInfo, latestEventId, threadId)) } if (openIntent != null) { setContentIntent(openIntent) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt index f8efa9b0e7..6a590aeda8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt @@ -16,6 +16,7 @@ import androidx.core.app.RemoteInput import io.element.android.appconfig.NotificationConfig import io.element.android.libraries.androidutils.uri.createIgnoredUri import io.element.android.libraries.di.ApplicationContext +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 @@ -33,11 +34,11 @@ class QuickReplyActionFactory @Inject constructor( private val stringProvider: StringProvider, private val clock: SystemClock, ) { - fun create(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): NotificationCompat.Action? { + fun create(roomInfo: RoomEventGroupInfo, eventId: EventId?, threadId: ThreadId?): NotificationCompat.Action? { if (!NotificationConfig.SHOW_QUICK_REPLY_ACTION) return null val sessionId = roomInfo.sessionId val roomId = roomInfo.roomId - val replyPendingIntent = buildQuickReplyIntent(sessionId, roomId, threadId) + val replyPendingIntent = buildQuickReplyIntent(sessionId, roomId, eventId, threadId) val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) .setLabel(stringProvider.getString(R.string.notification_room_action_quick_reply)) .build() @@ -63,6 +64,7 @@ class QuickReplyActionFactory @Inject constructor( private fun buildQuickReplyIntent( sessionId: SessionId, roomId: RoomId, + eventId: EventId?, threadId: ThreadId?, ): PendingIntent { val intent = Intent(context, NotificationBroadcastReceiver::class.java) @@ -70,9 +72,8 @@ class QuickReplyActionFactory @Inject constructor( intent.data = createIgnoredUri("quickReply/$sessionId/$roomId" + threadId?.let { "/$it" }.orEmpty()) intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value) - threadId?.let { - intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value) - } + eventId?.let { intent.putExtra(NotificationBroadcastReceiver.KEY_EVENT_ID, it.value) } + threadId?.let { intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value) } return PendingIntent.getBroadcast( context, 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 0ce6038d79..9405e19464 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 @@ -14,8 +14,9 @@ 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.asEventId import io.element.android.libraries.matrix.api.room.IntentionalMention +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.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -330,7 +331,8 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test send reply`() = runTest { val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } - val replyMessage = lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } + val replyMessage = + lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } val liveTimeline = FakeTimeline().apply { sendMessageLambda = sendMessage replyMessageLambda = replyMessage @@ -396,7 +398,8 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test send reply to thread`() = runTest { val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } - val replyMessage = lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } + val replyMessage = + lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } val liveTimeline = FakeTimeline().apply { sendMessageLambda = sendMessage replyMessageLambda = replyMessage @@ -423,6 +426,7 @@ class NotificationBroadcastReceiverHandlerTest { createIntent( action = actionIds.smartReply, roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, threadId = A_THREAD_ID, ), ) @@ -433,7 +437,13 @@ class NotificationBroadcastReceiverHandlerTest { .isCalledOnce() replyMessage.assertions() .isCalledOnce() - .with(value(A_THREAD_ID.asEventId()), value(A_MESSAGE), value(null), value(emptyList()), value(true)) + .with( + value(replyInThread(eventId = AN_EVENT_ID, explicitReply = false)), + value(A_MESSAGE), + value(null), + value(emptyList()), + value(true) + ) } private fun createIntent(