fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.7 (#4548)

* 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 <jorgem@element.io>
This commit is contained in:
renovate[bot]
2025-04-08 14:21:49 +02:00
committed by GitHub
parent 13a0467da2
commit 9981ae7b1e
35 changed files with 454 additions and 296 deletions

View File

@@ -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<SendActionState>,
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 = {

View File

@@ -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,
),
)
}
}
}
}

View File

@@ -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<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
val sendFileResult =
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
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<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
val sendFileResult =
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
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<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
val sendFileResult =
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
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<File, File?, ImageInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
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<File, File?, VideoInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
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<File, AudioInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
lambdaRecorder<File, AudioInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
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<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
val sendFileResult =
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
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<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
val sendFileResult =
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
Result.failure(failure)
}
val onDoneListenerResult = lambdaRecorder<Unit> {}

View File

@@ -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<IntentionalMention>, _: Boolean ->
val replyMessageLambda = lambdaRecorder { _: ReplyParameters, _: String, _: String?, _: List<IntentionalMention>, _: 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<IntentionalMention>, _: Boolean ->
val replyMessageLambda = lambdaRecorder { _: ReplyParameters, _: String, _: String?, _: List<IntentionalMention>, _: Boolean ->
Result.success(Unit)
}
val editMessageLambda = lambdaRecorder { _: EventOrTransactionId, _: String, _: String?, _: List<IntentionalMention> ->

View File

@@ -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<File, AudioInfo, List<Float>, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _ ->
lambdaRecorder<File, AudioInfo, List<Float>, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
private val matrixRoom = FakeMatrixRoom(

View File

@@ -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<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
val sendFileResult =
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val matrixRoom = FakeMatrixRoom(

View File

@@ -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" }

View File

@@ -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?,

View File

@@ -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<MediaUploadHandler>
suspend fun sendVideo(
@@ -147,7 +149,8 @@ interface MatrixRoom : Closeable {
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendAudio(
@@ -156,6 +159,7 @@ interface MatrixRoom : Closeable {
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendFile(
@@ -164,8 +168,36 @@ interface MatrixRoom : Closeable {
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
/**
* 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<Unit>
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
@@ -235,25 +267,6 @@ interface MatrixRoom : Closeable {
*/
suspend fun clearEventCacheStorage(): Result<Unit>
/**
* 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<Unit>
/**
* Create a poll in the room.
*
@@ -302,13 +315,6 @@ interface MatrixRoom : Closeable {
*/
suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit>
suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler>
/**
* Send a typing notification.
* @param isTyping True if the user is typing, false otherwise.

View File

@@ -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,
)

View File

@@ -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<Unit>
suspend fun replyMessage(
eventId: EventId,
replyParameters: ReplyParameters,
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
@@ -88,7 +89,8 @@ interface Timeline : AutoCloseable {
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendVideo(
@@ -97,17 +99,17 @@ interface Timeline : AutoCloseable {
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result<Unit>
suspend fun sendAudio(
file: File,
audioInfo: AudioInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendFile(
@@ -116,15 +118,9 @@ interface Timeline : AutoCloseable {
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
suspend fun cancelSend(transactionId: TransactionId): Result<Unit> =
redactEvent(transactionId.toEventOrTransactionId(), reason = null)
/**
* Share a location message in the room.
*
@@ -144,6 +140,23 @@ interface Timeline : AutoCloseable {
assetType: AssetType? = null,
): Result<Unit>
suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result<Unit>
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
suspend fun cancelSend(transactionId: TransactionId): Result<Unit> =
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<Unit>
suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler>
suspend fun loadReplyDetails(eventId: EventId): InReplyTo
/**

View File

@@ -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)

View File

@@ -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,
)
}

View File

@@ -324,7 +324,7 @@ class RustMatrixAuthenticationService @Inject constructor(
passphrase = pendingPassphrase,
slidingSyncType = ClientBuilderSlidingSync.Discovered,
)
.passphrase(passphrase)
.sessionPassphrase(passphrase)
.buildWithQrCode(qrCodeData, oidcConfiguration, progressListener)
}

View File

@@ -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,

View File

@@ -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<MediaUploadHandler> {
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<MediaUploadHandler> {
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<MediaUploadHandler> {
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<MediaUploadHandler> {
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<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
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<Unit> {
return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType)
}
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> {
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<Unit> {
return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType)
}
override suspend fun createPoll(
question: String,
answers: List<String>,
@@ -674,15 +713,6 @@ class RustMatrixRoom(
return liveTimeline.endPoll(pollStartId, text)
}
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> {
return liveTimeline.sendVoiceMessage(file, audioInfo, waveform, progressCallback)
}
override suspend fun typingNotice(isTyping: Boolean) = withContext(roomDispatcher) {
runCatching {
innerRoom.typingNotice(isTyping)

View File

@@ -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,
)

View File

@@ -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<IntentionalMention>,
@@ -336,7 +338,10 @@ class RustTimeline(
): Result<Unit> = 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<MediaUploadHandler> {
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<MediaUploadHandler> {
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<MediaUploadHandler> {
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<MediaUploadHandler> {
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<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
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<String>,
@@ -542,30 +581,6 @@ class RustTimeline(
}
}
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> {
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<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> {
return runCatching {
MediaUploadHandlerImpl(files, handle())

View File

@@ -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
}
}
}

View File

@@ -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<MatrixTimelineItem>): List<MatrixTimelineItem> {
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<MatrixTimelineItem>, roomCreator: UserId?, hasMoreToLoadBackwards: Boolean): List<MatrixTimelineItem> {
val roomBeginningItemIndex = if (!hasMoreToLoadBackwards) {
items.indexOfFirst { it is MatrixTimelineItem.Virtual && it.virtual is VirtualTimelineItem.RoomBeginning }.takeIf { it >= 0 }
} else {
null
}
private fun processForDM(items: List<MatrixTimelineItem>, roomCreator: UserId?): List<MatrixTimelineItem> {
// 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
}.takeIf { it >= 0 }
}
} ?: -1
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
)
}
}

View File

@@ -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)
}
}

View File

@@ -48,7 +48,7 @@ class RustMatrixAuthenticationServiceTest {
sessionStore = sessionStore,
rustMatrixClientFactory = rustMatrixClientFactory,
passphraseGenerator = FakePassphraseGenerator(),
oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory),
oidcConfigurationProvider = OidcConfigurationProvider(),
)
}
}

View File

@@ -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

View File

@@ -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))

View File

@@ -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(

View File

@@ -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,

View File

@@ -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<Boolean> = { lambdaError() },
private val canSendStateResult: (UserId, StateEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
private val canUserSendMessageResult: (UserId, MessageEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
private val sendImageResult: (File, File?, ImageInfo, String?, String?, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
private val sendImageResult: (File, File?, ImageInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _, _, _ -> lambdaError() },
private val sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _, _, _ -> lambdaError() },
private val sendFileResult: (File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _, _ -> lambdaError() },
private val sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _, _ -> lambdaError() },
private val sendFileResult: (File, FileInfo, String?, String?, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
private val sendVoiceMessageResult: (File, AudioInfo, List<Float>, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _ -> lambdaError() },
private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _ -> lambdaError() },
private val sendVoiceMessageResult: (File, AudioInfo, List<Float>, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _ -> lambdaError() },
private val setNameResult: (String) -> Result<Unit> = { lambdaError() },
private val setTopicResult: (String) -> Result<Unit> = { lambdaError() },
private val updateAvatarResult: (String, ByteArray) -> Result<Unit> = { _, _ -> lambdaError() },
@@ -332,7 +333,8 @@ class FakeMatrixRoom(
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = 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<MediaUploadHandler> = 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<MediaUploadHandler> = 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<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendFileResult(
@@ -395,6 +403,40 @@ class FakeMatrixRoom(
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendVoiceMessageResult(
file,
audioInfo,
waveform,
progressCallback,
replyParameters,
)
}
override suspend fun sendLocation(
body: String,
geoUri: String,
description: String?,
zoomLevel: Int?,
assetType: AssetType?,
): Result<Unit> = 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<Unit> = simulateLongTask {
return sendLocationResult(
body,
geoUri,
description,
zoomLevel,
assetType,
)
}
override suspend fun createPoll(
question: String,
answers: List<String>,
@@ -524,21 +550,6 @@ class FakeMatrixRoom(
return endPollResult(pollStartId, text)
}
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendVoiceMessageResult(
file,
audioInfo,
waveform,
progressCallback,
)
}
override suspend fun typingNotice(isTyping: Boolean): Result<Unit> {
return typingNoticeResult(isTyping)
}

View File

@@ -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<IntentionalMention>,
@@ -120,13 +121,13 @@ class FakeTimeline(
}
override suspend fun replyMessage(
eventId: EventId,
replyParameters: ReplyParameters,
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
fromNotification: Boolean,
): Result<Unit> = replyMessageLambda(
eventId,
replyParameters,
body,
htmlBody,
intentionalMentions,
@@ -140,7 +141,8 @@ class FakeTimeline(
body: String?,
formattedBody: String?,
progressCallback: ProgressCallback?,
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _ ->
replyParameters: ReplyParameters?,
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
@@ -151,13 +153,15 @@ class FakeTimeline(
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendImageLambda(
file,
thumbnailFile,
imageInfo,
caption,
formattedCaption,
progressCallback
progressCallback,
replyParameters,
)
var sendVideoLambda: (
@@ -167,7 +171,8 @@ class FakeTimeline(
body: String?,
formattedBody: String?,
progressCallback: ProgressCallback?,
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _ ->
replyParameters: ReplyParameters?,
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
@@ -178,13 +183,15 @@ class FakeTimeline(
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendVideoLambda(
file,
thumbnailFile,
videoInfo,
caption,
formattedCaption,
progressCallback
progressCallback,
replyParameters,
)
var sendAudioLambda: (
@@ -193,7 +200,8 @@ class FakeTimeline(
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
) -> Result<MediaUploadHandler> = { _, _, _, _, _ ->
replyParameters: ReplyParameters?,
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
@@ -203,12 +211,14 @@ class FakeTimeline(
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendAudioLambda(
file,
audioInfo,
caption,
formattedCaption,
progressCallback
progressCallback,
replyParameters,
)
var sendFileLambda: (
@@ -217,7 +227,8 @@ class FakeTimeline(
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
) -> Result<MediaUploadHandler> = { _, _, _, _, _ ->
replyParameters: ReplyParameters?,
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
@@ -227,22 +238,39 @@ class FakeTimeline(
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendFileLambda(
file,
fileInfo,
caption,
formattedCaption,
progressCallback
progressCallback,
replyParameters,
)
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = toggleReactionLambda(
emoji,
eventOrTransactionId
)
var sendVoiceMessageLambda: (
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
) -> Result<MediaUploadHandler> = { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendVoiceMessageLambda(
file,
audioInfo,
waveform,
progressCallback,
replyParameters,
)
var sendLocationLambda: (
body: String,
@@ -268,6 +296,17 @@ class FakeTimeline(
assetType
)
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = toggleReactionLambda(
emoji,
eventOrTransactionId
)
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
var createPollLambda: (
question: String,
answers: List<String>,
@@ -337,27 +376,6 @@ class FakeTimeline(
text: String,
): Result<Unit> = endPollLambda(pollStartId, text)
var sendVoiceMessageLambda: (
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
) -> Result<MediaUploadHandler> = { _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> = sendVoiceMessageLambda(
file,
audioInfo,
waveform,
progressCallback
)
var sendReadReceiptLambda: (
eventId: EventId,
receiptType: ReceiptType,

View File

@@ -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<Unit> {
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<Unit> {
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<Float>,
progressCallback: ProgressCallback? = null
progressCallback: ProgressCallback? = null,
replyParameters: ReplyParameters? = null,
): Result<Unit> {
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<Unit> {
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,
)
}
}

View File

@@ -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<File, File?, ImageInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
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<File, File?, ImageInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
Result.failure(Exception())
lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? ->
Result.failure<FakeMediaUploadHandler>(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<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
val sendFileResult =
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val room = FakeMatrixRoom(

View File

@@ -102,6 +102,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
senderId = content.senderId,
roomId = roomId,
eventId = eventId,
threadId = threadId,
noisy = isNoisy,
timestamp = this.timestamp,
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,

View File

@@ -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(

View File

@@ -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)

View File

@@ -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,

View File

@@ -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<String, String?, List<IntentionalMention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
val replyMessage = lambdaRecorder<EventId, String, String?, List<IntentionalMention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
val replyMessage =
lambdaRecorder<ReplyParameters, String, String?, List<IntentionalMention>, Boolean, Result<Unit>> { _, _, _, _, _ -> 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<String, String?, List<IntentionalMention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
val replyMessage = lambdaRecorder<EventId, String, String?, List<IntentionalMention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
val replyMessage =
lambdaRecorder<ReplyParameters, String, String?, List<IntentionalMention>, Boolean, Result<Unit>> { _, _, _, _, _ -> 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<IntentionalMention>()), value(true))
.with(
value(replyInThread(eventId = AN_EVENT_ID, explicitReply = false)),
value(A_MESSAGE),
value(null),
value(emptyList<IntentionalMention>()),
value(true)
)
}
private fun createIntent(