Merge branch 'develop' into feature/fga/user_moderation_bottomsheet

This commit is contained in:
ganfra 2025-05-23 17:28:48 +02:00
commit 03e1926e14
250 changed files with 3339 additions and 2742 deletions

View file

@ -162,6 +162,11 @@ interface MatrixClient {
fun canDeactivateAccount(): Boolean
suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit>
/**
* Check if the user can report a room.
*/
suspend fun canReportRoom(): Boolean
}
/**

View file

@ -43,7 +43,10 @@ interface MatrixAuthenticationService {
/**
* Get the Oidc url to display to the user.
*/
suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails>
suspend fun getOidcUrl(
prompt: OidcPrompt,
loginHint: String?,
): Result<OidcDetails>
/**
* Cancel Oidc login sequence.

View file

@ -7,4 +7,6 @@
package io.element.android.libraries.matrix.api.auth.qrlogin
interface MatrixQrCodeLoginData
interface MatrixQrCodeLoginData {
fun serverName(): String?
}

View file

@ -55,6 +55,12 @@ interface BaseRoom : Closeable {
*/
fun info(): RoomInfo = roomInfoFlow.value
/**
* A one-to-one is a room with exactly 2 members.
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
*/
val isOneToOne: Boolean get() = info().activeMembersCount == 2L
/**
* Try to load the room members and update the membersFlow.
*/

View file

@ -9,34 +9,21 @@ package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SendHandle
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.io.File
interface JoinedRoom : BaseRoom {
val syncUpdateFlow: StateFlow<Long>
@ -50,12 +37,6 @@ interface JoinedRoom : BaseRoom {
*/
val knockRequestsFlow: Flow<List<KnockRequest>>
/**
* A one-to-one is a room with exactly 2 members.
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
*/
val isOneToOne: Boolean get() = info().activeMembersCount == 2L
/**
* The live timeline of the room. Must be used to send Event to a room.
*/
@ -69,135 +50,14 @@ interface JoinedRoom : BaseRoom {
createTimelineParams: CreateTimelineParams,
): Result<Timeline>
suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit>
suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit>
suspend fun sendImage(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendVideo(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendAudio(
file: File,
audioInfo: AudioInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendFile(
file: File,
fileInfo: FileInfo,
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>
/**
* Create a poll in the room.
*
* @param question The question to ask.
* @param answers The list of answers.
* @param maxSelections The maximum number of answers that can be selected.
* @param pollKind The kind of poll to create.
*/
suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit>
/**
* Edit a poll in the room.
*
* @param pollStartId The event ID of the poll start event.
* @param question The question to ask.
* @param answers The list of answers.
* @param maxSelections The maximum number of answers that can be selected.
* @param pollKind The kind of poll to create.
*/
suspend fun editPoll(
pollStartId: EventId,
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit>
/**
* Send a response to a poll.
*
* @param pollStartId The event ID of the poll start event.
* @param answers The list of answer ids to send.
*/
suspend fun sendPollResponse(pollStartId: EventId, answers: List<String>): Result<Unit>
/**
* Ends a poll in the room.
*
* @param pollStartId The event ID of the poll start event.
* @param text Fallback text of the poll end event.
*/
suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit>
/**
* Send a typing notification.
* @param isTyping True if the user is typing, false otherwise.
*/
suspend fun typingNotice(isTyping: Boolean): 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>
suspend fun inviteUserById(id: UserId): Result<Unit>
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>

View file

@ -666,6 +666,12 @@ class RustMatrixClient(
}
}
override suspend fun canReportRoom(): Boolean = withContext(sessionDispatcher) {
runCatching {
innerClient.isReportRoomApiSupported()
}.getOrDefault(false)
}
private suspend fun File.getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {

View file

@ -187,14 +187,17 @@ class RustMatrixAuthenticationService @Inject constructor(
private var pendingOAuthAuthorizationData: OAuthAuthorizationData? = null
override suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails> {
override suspend fun getOidcUrl(
prompt: OidcPrompt,
loginHint: String?,
): Result<OidcDetails> {
return withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val oAuthAuthorizationData = client.urlForOidc(
oidcConfiguration = oidcConfigurationProvider.get(),
prompt = prompt.toRustPrompt(),
loginHint = null,
loginHint = loginHint,
)
val url = oAuthAuthorizationData.loginUrl()
pendingOAuthAuthorizationData = oAuthAuthorizationData

View file

@ -12,4 +12,8 @@ import org.matrix.rustcomponents.sdk.QrCodeData as RustQrCodeData
class SdkQrCodeLoginData(
internal val rustQrCodeData: RustQrCodeData,
) : MatrixQrCodeLoginData
) : MatrixQrCodeLoginData {
override fun serverName(): String? {
return rustQrCodeData.serverName()
}
}

View file

@ -13,20 +13,11 @@ import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SendHandle
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.IntentionalMention
@ -35,14 +26,11 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsStat
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.impl.core.RustSendHandle
@ -59,11 +47,8 @@ import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
@ -71,12 +56,10 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.DateDividerMode
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
import org.matrix.rustcomponents.sdk.KnockRequestsListener
import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
import org.matrix.rustcomponents.sdk.TimelineConfiguration
import org.matrix.rustcomponents.sdk.TimelineFilter
@ -89,11 +72,9 @@ import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk.RoomPowerLevelChanges
import java.io.File
import kotlin.coroutines.cancellation.CancellationException
import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange
import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest
import org.matrix.rustcomponents.sdk.RoomInfo as InnerRoomInfo
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
class JoinedRustRoom(
@ -101,7 +82,6 @@ class JoinedRustRoom(
private val liveInnerTimeline: InnerTimeline,
private val notificationSettingsService: NotificationSettingsService,
private val coroutineDispatchers: CoroutineDispatchers,
private val roomInfoMapper: RoomInfoMapper,
private val systemClock: SystemClock,
private val roomContentForwarder: RoomContentForwarder,
private val featureFlagService: FeatureFlagService,
@ -112,14 +92,6 @@ class JoinedRustRoom(
override val syncUpdateFlow = MutableStateFlow(0L)
override val roomInfoFlow: StateFlow<io.element.android.libraries.matrix.api.room.RoomInfo> = mxCallbackFlow {
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
override fun call(roomInfo: InnerRoomInfo) {
channel.trySend(roomInfoMapper.map(roomInfo))
}
})
}.stateIn(roomCoroutineScope, started = SharingStarted.Lazily, initialValue = baseRoom.info())
override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow {
val initial = emptyList<UserId>()
channel.trySend(initial)
@ -264,10 +236,6 @@ class JoinedRustRoom(
}
}
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit> {
return liveTimeline.sendMessage(body, htmlBody, intentionalMentions)
}
override suspend fun editMessage(
eventId: EventId,
body: String,
@ -281,159 +249,12 @@ class JoinedRustRoom(
}
}
override suspend fun sendImage(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
return liveTimeline.sendImage(
file = file,
thumbnailFile = thumbnailFile,
imageInfo = imageInfo,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback,
replyParameters = replyParameters
)
}
override suspend fun sendVideo(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
return liveTimeline.sendVideo(
file = file,
thumbnailFile = thumbnailFile,
videoInfo = videoInfo,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback,
replyParameters = replyParameters
)
}
override suspend fun sendAudio(
file: File,
audioInfo: AudioInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
return liveTimeline.sendAudio(
file = file,
audioInfo = audioInfo,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback,
replyParameters = replyParameters,
)
}
override suspend fun sendFile(
file: File,
fileInfo: FileInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
return liveTimeline.sendFile(
file = file,
fileInfo = fileInfo,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback,
replyParameters = replyParameters,
)
}
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<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 createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> {
return liveTimeline.createPoll(question, answers, maxSelections, pollKind)
}
override suspend fun editPoll(
pollStartId: EventId,
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> {
return liveTimeline.editPoll(pollStartId, question, answers, maxSelections, pollKind)
}
override suspend fun sendPollResponse(
pollStartId: EventId,
answers: List<String>
): Result<Unit> {
return liveTimeline.sendPollResponse(pollStartId, answers)
}
override suspend fun endPoll(
pollStartId: EventId,
text: String
): Result<Unit> {
return liveTimeline.endPoll(pollStartId, text)
}
override suspend fun typingNotice(isTyping: Boolean) = withContext(roomDispatcher) {
runCatching {
innerRoom.typingNotice(isTyping)
}
}
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> {
return liveTimeline.toggleReaction(emoji, eventOrTransactionId)
}
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> {
return liveTimeline.forwardEvent(eventId, roomIds)
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> {
return liveTimeline.cancelSend(transactionId)
}
override suspend fun inviteUserById(id: UserId): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.inviteUserById(id.value)
@ -645,7 +466,6 @@ class JoinedRustRoom(
override fun destroy() {
baseRoom.destroy()
liveInnerTimeline.destroy()
roomCoroutineScope.cancel()
}
private fun InnerTimeline.map(

View file

@ -31,10 +31,14 @@ import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper
import io.element.android.libraries.matrix.impl.roomdirectory.map
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk_base.EncryptionState
@ -48,6 +52,7 @@ class RustBaseRoom(
private val roomSyncSubscriber: RoomSyncSubscriber,
private val roomMembershipObserver: RoomMembershipObserver,
sessionCoroutineScope: CoroutineScope,
roomInfoMapper: RoomInfoMapper,
initialRoomInfo: RoomInfo,
) : BaseRoom {
override val roomId = RoomId(innerRoom.id())
@ -62,10 +67,16 @@ class RustBaseRoom(
override val membersStateFlow: StateFlow<RoomMembersState> = roomMemberListFetcher.membersFlow
override val roomInfoFlow: StateFlow<RoomInfo> = MutableStateFlow(initialRoomInfo)
override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
override val roomInfoFlow: StateFlow<RoomInfo> = mxCallbackFlow {
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
override fun call(roomInfo: org.matrix.rustcomponents.sdk.RoomInfo) {
channel.trySend(roomInfoMapper.map(roomInfo))
}
})
}.stateIn(roomCoroutineScope, started = SharingStarted.Lazily, initialValue = initialRoomInfo)
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
override suspend fun updateMembers() {
@ -98,6 +109,7 @@ class RustBaseRoom(
override fun destroy() {
innerRoom.destroy()
roomCoroutineScope.cancel()
}
override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) {

View file

@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl.room
import io.element.android.appconfig.TimelineConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.RoomId
@ -21,6 +20,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
@ -30,10 +30,10 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import org.matrix.rustcomponents.sdk.Room as SdkRoom
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
class RustRoomFactory(
@ -56,11 +56,6 @@ class RustRoomFactory(
private val mutex = Mutex()
private val isDestroyed: AtomicBoolean = AtomicBoolean(false)
private data class RustRoomReferences(
val roomListItem: RoomListItem,
val room: SdkRoom,
)
private val roomInfoMapper = RoomInfoMapper()
private val eventFilters = TimelineConfig.excludedEvents
@ -84,20 +79,26 @@ class RustRoomFactory(
Timber.d("Room factory is destroyed, returning null for $roomId")
return@withContext null
}
val roomReferences = awaitRoomReferences(roomId) ?: return@withContext null
getBaseRoom(roomReferences)
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
getBaseRoom(roomListItem)
}
}
private suspend fun getBaseRoom(roomReferences: RustRoomReferences): RustBaseRoom? {
val initialRoomInfo = roomReferences.room.roomInfo()
private suspend fun getBaseRoom(roomListItem: RoomListItem): RustBaseRoom? {
val sdkRoom = innerClient.getRoom(roomListItem.id()) ?: return null
return getBaseRoom(sdkRoom)
}
private suspend fun getBaseRoom(sdkRoom: Room): RustBaseRoom {
val initialRoomInfo = sdkRoom.roomInfo()
return RustBaseRoom(
sessionId = sessionId,
deviceId = deviceId,
innerRoom = roomReferences.room,
innerRoom = sdkRoom,
coroutineDispatchers = dispatchers,
roomSyncSubscriber = roomSyncSubscriber,
roomMembershipObserver = roomMembershipObserver,
roomInfoMapper = roomInfoMapper,
initialRoomInfo = roomInfoMapper.map(initialRoomInfo),
sessionCoroutineScope = sessionCoroutineScope,
)
@ -109,31 +110,26 @@ class RustRoomFactory(
Timber.d("Room factory is destroyed, returning null for $roomId")
return@withContext null
}
val roomReferences = awaitRoomReferences(roomId) ?: return@withContext null
if (roomReferences.room.membership() == Membership.JOINED) {
val baseRoom = getBaseRoom(roomReferences) ?: return@withContext null
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
if (roomListItem.membership() == Membership.JOINED) {
// Init the live timeline in the SDK from the RoomListItem
if (!roomReferences.roomListItem.isTimelineInitialized()) {
roomReferences.roomListItem.initTimeline(eventFilters, "LIVE")
}
val sdkRoom = roomListItem.fullRoomWithTimeline(eventFilters)
GetRoomResult.Joined(
JoinedRustRoom(
baseRoom = baseRoom,
baseRoom = getBaseRoom(sdkRoom),
notificationSettingsService = notificationSettingsService,
roomContentForwarder = roomContentForwarder,
liveInnerTimeline = roomReferences.room.timeline(),
liveInnerTimeline = sdkRoom.timeline(),
coroutineDispatchers = dispatchers,
systemClock = systemClock,
roomInfoMapper = roomInfoMapper,
featureFlagService = featureFlagService,
)
)
} else {
val preview = try {
roomReferences.roomListItem.previewRoom(via = emptyList())
roomListItem.previewRoom(via = emptyList())
} catch (e: Exception) {
Timber.e(e, "Failed to get room preview for $roomId")
return@withContext null
@ -142,7 +138,7 @@ class RustRoomFactory(
GetRoomResult.NotJoined(
NotJoinedRustRoom(
sessionId = sessionId,
localRoom = getBaseRoom(roomReferences),
localRoom = getBaseRoom(roomListItem),
previewInfo = RoomPreviewInfoMapper.map(preview.info()),
)
)
@ -150,36 +146,23 @@ class RustRoomFactory(
}
}
private fun getRoomReferences(roomId: RoomId): RustRoomReferences? {
val roomListItem = innerRoomListService.roomOrNull(roomId.value)
/**
* Get the Rust room list item for a room, retrying after the room list is loaded if necessary.
*/
private suspend fun awaitRoomListItem(roomId: RoomId): RoomListItem? {
var roomListItem = innerRoomListService.roomOrNull(roomId.value)
if (roomListItem == null) {
// ... otherwise, lets wait for the SS to load all rooms and check again.
roomListService.allRooms.awaitLoaded()
roomListItem = innerRoomListService.roomOrNull(roomId.value)
}
if (roomListItem == null) {
Timber.d("Room not found for $roomId")
return null
}
val room = tryOrNull {
innerClient.getRoom(roomId.value)
} ?: error("Failed to get room for room id: $roomId")
Timber.d("Got room for $roomId")
return RustRoomReferences(
roomListItem = roomListItem,
room = room,
)
}
/**
* Get the Rust room references for a room, retrying after the room list is loaded if necessary.
*/
private suspend fun awaitRoomReferences(roomId: RoomId): RustRoomReferences? {
var roomReferences = getRoomReferences(roomId)
if (roomReferences == null) {
// ... otherwise, lets wait for the SS to load all rooms and check again.
roomListService.allRooms.awaitLoaded()
roomReferences = getRoomReferences(roomId)
}
return roomReferences
return roomListItem
}
}

View file

@ -49,6 +49,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG",
parentUrl = null,
hideHeader = true,
controlledMediaDevices = false,
)
val rustWidgetSettings = newVirtualElementCallWidget(options)
return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings)

View file

@ -0,0 +1,35 @@
/*
* 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.auth.qrlogin
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeQrCodeData
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import org.junit.Test
class SdkQrCodeLoginDataTest {
@Test
fun `getServer reads the value from the Rust side, null case`() {
val sut = SdkQrCodeLoginData(
rustQrCodeData = FakeQrCodeData(
serverNameResult = { null },
),
)
assertThat(sut.serverName()).isNull()
}
@Test
fun `getServer reads the value from the Rust side`() {
val sut = SdkQrCodeLoginData(
rustQrCodeData = FakeQrCodeData(
serverNameResult = { A_HOMESERVER_URL },
),
)
assertThat(sut.serverName()).isEqualTo(A_HOMESERVER_URL)
}
}

View file

@ -0,0 +1,20 @@
/*
* 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.fixtures.fakes
import io.element.android.tests.testutils.lambda.lambdaError
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.QrCodeData
class FakeQrCodeData(
private val serverNameResult: () -> String? = { lambdaError() },
) : QrCodeData(NoPointer) {
override fun serverName(): String? {
return serverNameResult()
}
}

View file

@ -0,0 +1,55 @@
/*
* 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
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class RustBaseRoomTest {
@Test
fun `RustBaseRoom should cancel the room coroutine scope when it is destroyed`() = runTest {
val rustBaseRoom = createRustBaseRoom(
// Not using backgroundScope here, but the test scope
sessionCoroutineScope = this
)
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isTrue()
rustBaseRoom.destroy()
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse()
}
private fun TestScope.createRustBaseRoom(
sessionCoroutineScope: CoroutineScope,
): RustBaseRoom {
val dispatchers = testCoroutineDispatchers()
return RustBaseRoom(
sessionId = A_SESSION_ID,
deviceId = A_DEVICE_ID,
innerRoom = FakeRustRoom(),
coroutineDispatchers = dispatchers,
roomSyncSubscriber = RoomSyncSubscriber(
roomListService = FakeRustRoomListService(),
dispatchers = dispatchers,
),
roomMembershipObserver = RoomMembershipObserver(),
sessionCoroutineScope = sessionCoroutineScope,
roomInfoMapper = RoomInfoMapper(),
initialRoomInfo = aRoomInfo(),
)
}
}

View file

@ -88,6 +88,7 @@ class FakeMatrixClient(
private val availableSlidingSyncVersionsLambda: () -> Result<List<SlidingSyncVersion>> = { lambdaError() },
private val ignoreUserResult: (UserId) -> Result<Unit> = { lambdaError() },
private var unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
private val canReportRoomLambda: () -> Boolean = { false },
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
@ -329,4 +330,8 @@ class FakeMatrixClient(
override suspend fun availableSlidingSyncVersions(): Result<List<SlidingSyncVersion>> {
return availableSlidingSyncVersionsLambda()
}
override suspend fun canReportRoom(): Boolean {
return canReportRoomLambda()
}
}

View file

@ -19,7 +19,6 @@ import io.element.android.libraries.matrix.api.core.TransactionId
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.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
const val A_USER_NAME = "alice"
const val A_USER_NAME_2 = "Bob"
@ -73,7 +72,6 @@ const val A_HOMESERVER_URL_2 = "matrix-client.org"
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE
val A_ROOM_NOTIFICATION_SETTINGS = RoomNotificationSettings(mode = A_ROOM_NOTIFICATION_MODE, isDefault = false)
const val AN_AVATAR_URL = "mxc://data"
@ -88,3 +86,5 @@ val A_SERVER_LIST = listOf("server1", "server2")
const val A_TIMESTAMP = 567L
const val A_FORMATTED_DATE = "April 6, 1980 at 6:35 PM"
const val A_LOGIN_HINT = "mxid:@alice:example.org"

View file

@ -87,7 +87,10 @@ class FakeMatrixAuthenticationService(
return importCreatedSessionLambda(externalSession)
}
override suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails> = simulateLongTask {
override suspend fun getOidcUrl(
prompt: OidcPrompt,
loginHint: String?,
): Result<OidcDetails> = simulateLongTask {
oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA)
}

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.test.auth.qrlogin
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
class FakeMatrixQrCodeLoginDataFactory(
@ -20,4 +21,8 @@ class FakeMatrixQrCodeLoginDataFactory(
}
}
class FakeMatrixQrCodeLoginData : MatrixQrCodeLoginData
class FakeMatrixQrCodeLoginData(
private val serverNameResult: () -> String? = { lambdaError() },
) : MatrixQrCodeLoginData {
override fun serverName() = serverNameResult()
}

View file

@ -12,17 +12,9 @@ import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SendHandle
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.IntentionalMention
@ -33,16 +25,12 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsStat
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.tests.testutils.lambda.lambdaError
@ -53,7 +41,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.TestScope
import java.io.File
class FakeJoinedRoom(
val baseRoom: FakeBaseRoom = FakeBaseRoom(),
@ -63,35 +50,16 @@ class FakeJoinedRoom(
override val roomTypingMembersFlow: Flow<List<UserId>> = MutableStateFlow(emptyList()),
override val identityStateChangesFlow: Flow<List<IdentityStateChange>> = MutableStateFlow(emptyList()),
override val roomNotificationSettingsStateFlow: StateFlow<RoomNotificationSettingsState> =
MutableStateFlow(RoomNotificationSettingsState.Unknown),
MutableStateFlow(RoomNotificationSettingsState.Unknown),
override val knockRequestsFlow: Flow<List<KnockRequest>> = MutableStateFlow(emptyList()),
private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
private var createTimelineResult: (CreateTimelineParams) -> Result<Timeline> = { lambdaError() },
private val sendMessageResult: (String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _ -> lambdaError() },
private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
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 sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _, _ -> lambdaError() },
private val sendVoiceMessageResult: (File, AudioInfo, List<Float>, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _ -> lambdaError() },
private val sendLocationResult: (String, String, String?, Int?, AssetType?) -> Result<Unit> = { _, _, _, _, _ -> lambdaError() },
private val sendCallNotificationIfNeededResult: () -> Result<Unit> = { lambdaError() },
private val progressCallbackValues: List<Pair<Long, Long>> = emptyList(),
private val createPollResult: (String, List<String>, Int, PollKind) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
private val editPollResult: (EventId, String, List<String>, Int, PollKind) -> Result<Unit> = { _, _, _, _, _ -> lambdaError() },
private val sendPollResponseResult: (EventId, List<String>) -> Result<Unit> = { _, _ -> lambdaError() },
private val endPollResult: (EventId, String) -> Result<Unit> = { _, _ -> lambdaError() },
private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result<String> = { _, _, _, _ -> lambdaError() },
private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result<MatrixWidgetDriver> = { lambdaError() },
private val typingNoticeResult: (Boolean) -> Result<Unit> = { lambdaError() },
private val toggleReactionResult: (String, EventOrTransactionId) -> Result<Unit> = { _, _ -> lambdaError() },
private val forwardEventResult: (EventId, List<RoomId>) -> Result<Unit> = { _, _ -> lambdaError() },
private val cancelSendResult: (TransactionId) -> Result<Unit> = { lambdaError() },
private val inviteUserResult: (UserId) -> Result<Unit> = { lambdaError() },
private val setNameResult: (String) -> Result<Unit> = { lambdaError() },
private val setTopicResult: (String) -> Result<Unit> = { lambdaError() },
@ -127,10 +95,6 @@ class FakeJoinedRoom(
createTimelineResult(createTimelineParams)
}
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit> = simulateLongTask {
sendMessageResult(body, htmlBody, intentionalMentions)
}
override suspend fun editMessage(
eventId: EventId,
body: String,
@ -140,174 +104,10 @@ class FakeJoinedRoom(
editMessageLambda(eventId, body, htmlBody, intentionalMentions)
}
override suspend fun sendImage(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendImageResult(
file,
thumbnailFile,
imageInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
override suspend fun sendVideo(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendVideoResult(
file,
thumbnailFile,
videoInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
override suspend fun sendAudio(
file: File,
audioInfo: AudioInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendAudioResult(
file,
audioInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
override suspend fun sendFile(
file: File,
fileInfo: FileInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendFileResult(
file,
fileInfo,
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,
)
}
override suspend fun createPoll(question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind): Result<Unit> = simulateLongTask {
return createPollResult(
question,
answers,
maxSelections,
pollKind,
)
}
override suspend fun editPoll(
pollStartId: EventId,
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind
): Result<Unit> = simulateLongTask {
return editPollResult(
pollStartId,
question,
answers,
maxSelections,
pollKind,
)
}
override suspend fun sendPollResponse(pollStartId: EventId, answers: List<String>): Result<Unit> = simulateLongTask {
return sendPollResponseResult(
pollStartId,
answers,
)
}
override suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit> = simulateLongTask {
endPollResult(
pollStartId,
text,
)
}
override suspend fun typingNotice(isTyping: Boolean): Result<Unit> = simulateLongTask {
typingNoticeResult(isTyping)
}
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = simulateLongTask {
toggleReactionResult(emoji, eventOrTransactionId)
}
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = simulateLongTask {
forwardEventResult(eventId, roomIds)
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = simulateLongTask {
cancelSendResult(transactionId)
}
override suspend fun inviteUserById(id: UserId): Result<Unit> = simulateLongTask {
inviteUserResult(id)
}

View file

@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.test.timeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
@ -26,6 +27,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -47,23 +50,31 @@ class FakeTimeline(
)
),
override val membershipChangeEventReceived: Flow<Unit> = MutableSharedFlow(),
private val progressCallbackValues: List<Pair<Long, Long>> = emptyList(),
private val cancelSendResult: (TransactionId) -> Result<Unit> = { lambdaError() },
) : Timeline {
var sendMessageLambda: (
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = simulateLongTask {
cancelSendResult(transactionId)
}
override suspend fun sendMessage(
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
): Result<Unit> = sendMessageLambda(body, htmlBody, intentionalMentions)
): Result<Unit> = simulateLongTask {
sendMessageLambda(body, htmlBody, intentionalMentions)
}
var redactEventLambda: (eventOrTransactionId: EventOrTransactionId, reason: String?) -> Result<Unit> = { _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun redactEvent(
@ -77,7 +88,7 @@ class FakeTimeline(
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
) -> Result<Unit> = { _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun editMessage(
@ -117,7 +128,7 @@ class FakeTimeline(
intentionalMentions: List<IntentionalMention>,
fromNotification: Boolean,
) -> Result<Unit> = { _, _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun replyMessage(
@ -154,15 +165,18 @@ class FakeTimeline(
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendImageLambda(
file,
thumbnailFile,
imageInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendImageLambda(
file,
thumbnailFile,
imageInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
var sendVideoLambda: (
file: File,
@ -184,15 +198,18 @@ class FakeTimeline(
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendVideoLambda(
file,
thumbnailFile,
videoInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendVideoLambda(
file,
thumbnailFile,
videoInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
var sendAudioLambda: (
file: File,
@ -212,14 +229,17 @@ class FakeTimeline(
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendAudioLambda(
file,
audioInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendAudioLambda(
file,
audioInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
var sendFileLambda: (
file: File,
@ -239,14 +259,17 @@ class FakeTimeline(
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendFileLambda(
file,
fileInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendFileLambda(
file,
fileInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
var sendVoiceMessageLambda: (
file: File,
@ -264,13 +287,16 @@ class FakeTimeline(
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendVoiceMessageLambda(
file,
audioInfo,
waveform,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendVoiceMessageLambda(
file,
audioInfo,
waveform,
progressCallback,
replyParameters,
)
}
var sendLocationLambda: (
body: String,
@ -279,7 +305,7 @@ class FakeTimeline(
zoomLevel: Int?,
assetType: AssetType?,
) -> Result<Unit> = { _, _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun sendLocation(
@ -288,24 +314,30 @@ class FakeTimeline(
description: String?,
zoomLevel: Int?,
assetType: AssetType?,
): Result<Unit> = sendLocationLambda(
body,
geoUri,
description,
zoomLevel,
assetType
)
): Result<Unit> = simulateLongTask {
sendLocationLambda(
body,
geoUri,
description,
zoomLevel,
assetType,
)
}
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> lambdaError() }
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = toggleReactionLambda(
emoji,
eventOrTransactionId
)
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = simulateLongTask {
toggleReactionLambda(
emoji,
eventOrTransactionId,
)
}
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> lambdaError() }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = simulateLongTask {
forwardEventLambda(eventId, roomIds)
}
var createPollLambda: (
question: String,
@ -313,20 +345,17 @@ class FakeTimeline(
maxSelections: Int,
pollKind: PollKind,
) -> Result<Unit> = { _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> = createPollLambda(
question,
answers,
maxSelections,
pollKind
)
override suspend fun createPoll(question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind): Result<Unit> = simulateLongTask {
createPollLambda(
question,
answers,
maxSelections,
pollKind,
)
}
var editPollLambda: (
pollStartId: EventId,
@ -335,7 +364,7 @@ class FakeTimeline(
maxSelections: Int,
pollKind: PollKind,
) -> Result<Unit> = { _, _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun editPoll(
@ -343,44 +372,56 @@ class FakeTimeline(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> = editPollLambda(
pollStartId,
question,
answers,
maxSelections,
pollKind
)
pollKind: PollKind
): Result<Unit> = simulateLongTask {
editPollLambda(
pollStartId,
question,
answers,
maxSelections,
pollKind,
)
}
var sendPollResponseLambda: (
pollStartId: EventId,
answers: List<String>,
) -> Result<Unit> = { _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun sendPollResponse(
pollStartId: EventId,
answers: List<String>,
): Result<Unit> = sendPollResponseLambda(pollStartId, answers)
): Result<Unit> = simulateLongTask {
sendPollResponseLambda(
pollStartId,
answers,
)
}
var endPollLambda: (
pollStartId: EventId,
text: String,
) -> Result<Unit> = { _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun endPoll(
pollStartId: EventId,
text: String,
): Result<Unit> = endPollLambda(pollStartId, text)
): Result<Unit> = simulateLongTask {
endPollLambda(
pollStartId,
text,
)
}
var sendReadReceiptLambda: (
eventId: EventId,
receiptType: ReceiptType,
) -> Result<Unit> = { _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun sendReadReceipt(
@ -417,5 +458,12 @@ class FakeTimeline(
closeCounter++
}
private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
progressCallbackValues.forEach { (current, total) ->
progressCallback?.onProgress(current, total)
delay(1)
}
}
override fun toString() = "FakeTimeline: $name"
}