Split MatrixRoom into BaseRoom and JoinedRoom (#4561)

`JoinedRoom` will now contain both a mandatory live timeline reference and all the functionality associated to it.

`BaseRoom` on the other hand will contain only functionality that's shared for both joined and not joined rooms.

`NotJoinedRoom` is a wrapper around `RoomPreviewInfo` data and a possible local `BaseRoom`, if it exists.

The `RustRoomFactory` cache is now gone since the persistent event cache should have the same effect.
This commit is contained in:
Jorge Martin Espinosa 2025-04-23 15:53:40 +02:00 committed by GitHub
parent 91cb84ce8d
commit 619aa6f2de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
193 changed files with 2921 additions and 2567 deletions

View file

@ -57,7 +57,7 @@ import org.robolectric.annotation.Config
@Suppress("LargeClass")
@RunWith(RobolectricTestRunner::class)
class DefaultRoomLastMessageFormatterTest {
class DefaultBaseRoomLastMessageFormatterTest {
private lateinit var context: Context
private lateinit var fakeMatrixClient: FakeMatrixClient
private lateinit var formatter: DefaultRoomLastMessageFormatter

View file

@ -23,10 +23,11 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.NotJoinedRoom
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomPreview
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
@ -52,8 +53,8 @@ interface MatrixClient {
val mediaLoader: MatrixMediaLoader
val sessionCoroutineScope: CoroutineScope
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
suspend fun getRoom(roomId: RoomId): MatrixRoom?
suspend fun getPendingRoom(roomId: RoomId): RoomPreview?
suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom?
suspend fun getRoom(roomId: RoomId): BaseRoom?
suspend fun findDM(userId: UserId): RoomId?
suspend fun ignoreUser(userId: UserId): Result<Unit>
suspend fun unignoreUser(userId: UserId): Result<Unit>
@ -147,7 +148,7 @@ interface MatrixClient {
/**
* Get a room preview for a given room ID or alias. This is especially useful for rooms that the user is not a member of, or hasn't joined yet.
*/
suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomPreview>
suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<NotJoinedRoom>
/**
* Returns the currently used sliding sync version.
@ -168,7 +169,7 @@ interface MatrixClient {
* The flow will emit a new value whenever the room info is updated.
* The flow will emit Optional.empty item if the room is not found.
*/
fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<MatrixRoomInfo>> {
fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<RoomInfo>> {
return getRoomSummaryFlow(roomIdOrAlias)
.map { roomSummary -> roomSummary.map { it.info } }
.distinctUntilChanged()

View file

@ -8,11 +8,11 @@
package io.element.android.libraries.matrix.api.analytics
import im.vector.app.features.analytics.plan.ViewRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
fun MatrixRoom.toAnalyticsViewRoom(
fun BaseRoom.toAnalyticsViewRoom(
trigger: ViewRoom.Trigger? = null,
selectedSpace: MatrixRoom? = null,
selectedSpace: BaseRoom? = null,
viaKeyboard: Boolean? = null,
): ViewRoom {
val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home
@ -26,6 +26,6 @@ fun MatrixRoom.toAnalyticsViewRoom(
)
}
private fun MatrixRoom.toActiveSpace(): ViewRoom.ActiveSpace {
private fun BaseRoom.toActiveSpace(): ViewRoom.ActiveSpace {
return if (info().isPublic) ViewRoom.ActiveSpace.Public else ViewRoom.ActiveSpace.Private
}

View file

@ -8,14 +8,14 @@
package io.element.android.libraries.matrix.api.notificationsettings
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState
import kotlinx.coroutines.flow.SharedFlow
interface NotificationSettingsService {
/**
* State of the current room notification settings flow ([MatrixRoomNotificationSettingsState.Unknown] if not started).
* State of the current room notification settings flow ([RoomNotificationSettingsState.Unknown] if not started).
*/
val notificationSettingsChangeFlow: SharedFlow<Unit>
suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings>

View file

@ -0,0 +1,231 @@
/*
* Copyright 2023, 2024 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
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.UserId
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import java.io.Closeable
/**
* This interface represents the common functionality for a local room, whether it's joined, invited, knocked, or left.
*/
interface BaseRoom : Closeable {
/**
* The session id of the current user.
*/
val sessionId: SessionId
/**
* The id of the room.
*/
val roomId: RoomId
/**
* The coroutine scope that will handle all jobs related to this room.
*/
val roomCoroutineScope: CoroutineScope
/**
* The current loaded members as a StateFlow.
* Initial value is [RoomMembersState.Unknown].
* To update them you should call [updateMembers].
*/
val membersStateFlow: StateFlow<RoomMembersState>
/**
* A flow that emits the current [RoomInfo] state.
*/
val roomInfoFlow: StateFlow<RoomInfo>
/**
* Get the latest room info we have received from the SDK stream.
*/
fun info(): RoomInfo = roomInfoFlow.value
/**
* Try to load the room members and update the membersFlow.
*/
suspend fun updateMembers()
/**
* Get the members of the room. Note: generally this should not be used, please use
* [membersStateFlow] and [updateMembers] instead.
*/
suspend fun getMembers(limit: Int = 5): Result<List<RoomMember>>
/**
* Will return an updated member or an error.
*/
suspend fun getUpdatedMember(userId: UserId): Result<RoomMember>
/**
* Adds the room to the sync subscription list.
*/
suspend fun subscribeToSync()
/**
* Gets the power levels of the room.
*/
suspend fun powerLevels(): Result<RoomPowerLevels>
/**
* Gets the role of the user with the provided [userId] in the room.
*/
suspend fun userRole(userId: UserId): Result<RoomMember.Role>
/**
* Gets the display name of the user with the provided [userId] in the room.
*/
suspend fun userDisplayName(userId: UserId): Result<String?>
/**
* Gets the avatar of the user with the provided [userId] in the room.
*/
suspend fun userAvatarUrl(userId: UserId): Result<String?>
/**
* Leaves and forgets the room. Only joined, invited or knocked rooms can be left.
*/
suspend fun leave(): Result<Unit>
/**
* Joins the room. Only invited rooms can be joined.
*/
suspend fun join(): Result<Unit>
/**
* Forgets about the room, removing it from the server and the local cache. Only left and banned rooms can be forgotten.
*/
suspend fun forget(): Result<Unit>
/**
* Returns `true` if the user with the provided [userId] can invite other users to the room.
*/
suspend fun canUserInvite(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can kick other users from the room.
*/
suspend fun canUserKick(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can ban other users from the room.
*/
suspend fun canUserBan(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can redact their own messages.
*/
suspend fun canUserRedactOwn(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can redact messages from other users.
*/
suspend fun canUserRedactOther(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can send state events.
*/
suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can send messages.
*/
suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can trigger an `@room` notification.
*/
suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can pin or unpin messages.
*/
suspend fun canUserPinUnpin(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can join or starts calls.
*/
suspend fun canUserJoinCall(userId: UserId): Result<Boolean> =
canUserSendState(userId, StateEventType.CALL_MEMBER)
/**
* Sets the room as favorite or not, based on the [isFavorite] parameter.
*/
suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit>
/**
* Mark the room as read by trying to attach an unthreaded read receipt to the latest room event.
* @param receiptType The type of receipt to send.
*/
suspend fun markAsRead(receiptType: ReceiptType): Result<Unit>
/**
* Sets a flag on the room to indicate that the user has explicitly marked it as unread, or reverts the flag.
* @param isUnread true to mark the room as unread, false to remove the flag.
*/
suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit>
/**
* Clear the event cache storage for the current room.
*/
suspend fun clearEventCacheStorage(): Result<Unit>
/**
* Get the permalink for the room.
*/
suspend fun getPermalink(): Result<String>
/**
* Get the permalink for the provided [eventId].
* @param eventId The event id to get the permalink for.
* @return The permalink, or a failure.
*/
suspend fun getPermalinkFor(eventId: EventId): Result<String>
/**
* Returns the visibility for this room in the room directory.
* If the room is not published, the result will be [RoomVisibility.Private].
*/
suspend fun getRoomVisibility(): Result<RoomVisibility>
/**
* Returns the visibility for this room in the room directory, fetching it from the homeserver if needed.
*/
suspend fun getUpdatedIsEncrypted(): Result<Boolean>
/**
* Store the given `ComposerDraft` in the state store of this room.
*/
suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result<Unit>
/**
* Retrieve the `ComposerDraft` stored in the state store for this room.
*/
suspend fun loadComposerDraft(): Result<ComposerDraft?>
/**
* Clear the `ComposerDraft` stored in the state store for this room.
*/
suspend fun clearComposerDraft(): Result<Unit>
/**
* Destroy the room and release all resources associated to it.
*/
fun destroy()
override fun close() = destroy()
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* 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.
@ -13,7 +13,6 @@ 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.SessionId
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
@ -23,35 +22,28 @@ 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.draft.ComposerDraft
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.MatrixRoomPowerLevels
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.ReceiptType
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.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.io.Closeable
import java.io.File
interface MatrixRoom : Closeable {
val sessionId: SessionId
val roomId: RoomId
interface JoinedRoom : BaseRoom {
val syncUpdateFlow: StateFlow<Long>
val roomCoroutineScope: CoroutineScope
val roomInfoFlow: StateFlow<MatrixRoomInfo>
val roomTypingMembersFlow: Flow<List<UserId>>
val identityStateChangesFlow: Flow<List<IdentityStateChange>>
val roomNotificationSettingsStateFlow: StateFlow<RoomNotificationSettingsState>
/**
* The current knock requests in the room as a Flow.
@ -64,40 +56,6 @@ interface MatrixRoom : Closeable {
*/
val isOneToOne: Boolean get() = info().activeMembersCount == 2L
/**
* The current loaded members as a StateFlow.
* Initial value is [MatrixRoomMembersState.Unknown].
* To update them you should call [updateMembers].
*/
val membersStateFlow: StateFlow<MatrixRoomMembersState>
val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState>
/**
* Get the latest room info we have received from the SDK stream.
*/
fun info(): MatrixRoomInfo = roomInfoFlow.value
/**
* Try to load the room members and update the membersFlow.
*/
suspend fun updateMembers()
/**
* Get the members of the room. Note: generally this should not be used, please use
* [membersStateFlow] and [updateMembers] instead.
*/
suspend fun getMembers(limit: Int = 5): Result<List<RoomMember>>
/**
* Will return an updated member or an error.
*/
suspend fun getUpdatedMember(userId: UserId): Result<RoomMember>
suspend fun updateRoomNotificationSettings(): Result<Unit>
val syncUpdateFlow: StateFlow<Long>
/**
* The live timeline of the room. Must be used to send Event to a room.
*/
@ -111,24 +69,6 @@ interface MatrixRoom : Closeable {
createTimelineParams: CreateTimelineParams,
): Result<Timeline>
fun destroy()
suspend fun subscribeToSync()
suspend fun powerLevels(): Result<MatrixRoomPowerLevels>
suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result<Unit>
suspend fun resetPowerLevels(): Result<MatrixRoomPowerLevels>
suspend fun userRole(userId: UserId): Result<RoomMember.Role>
suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit>
suspend fun userDisplayName(userId: UserId): Result<String?>
suspend fun userAvatarUrl(userId: UserId): Result<String?>
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>
@ -198,75 +138,6 @@ interface MatrixRoom : Closeable {
assetType: AssetType? = null,
): 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 leave(): Result<Unit>
suspend fun join(): Result<Unit>
suspend fun inviteUserById(id: UserId): Result<Unit>
suspend fun canUserInvite(userId: UserId): Result<Boolean>
suspend fun canUserKick(userId: UserId): Result<Boolean>
suspend fun canUserBan(userId: UserId): Result<Boolean>
suspend fun canUserRedactOwn(userId: UserId): Result<Boolean>
suspend fun canUserRedactOther(userId: UserId): Result<Boolean>
suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean>
suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result<Boolean>
suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean>
suspend fun canUserPinUnpin(userId: UserId): Result<Boolean>
suspend fun canUserJoinCall(userId: UserId): Result<Boolean> =
canUserSendState(userId, StateEventType.CALL_MEMBER)
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit>
suspend fun setName(name: String): Result<Unit>
suspend fun setTopic(topic: String): Result<Unit>
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
suspend fun kickUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun banUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun unbanUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit>
/**
* Mark the room as read by trying to attach an unthreaded read receipt to the latest room event.
* @param receiptType The type of receipt to send.
*/
suspend fun markAsRead(receiptType: ReceiptType): Result<Unit>
/**
* Sets a flag on the room to indicate that the user has explicitly marked it as unread, or reverts the flag.
* @param isUnread true to mark the room as unread, false to remove the flag.
*
*/
suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit>
/**
* Clear the event cache storage for the current room.
*/
suspend fun clearEventCacheStorage(): Result<Unit>
/**
* Create a poll in the room.
*
@ -321,82 +192,19 @@ interface MatrixRoom : Closeable {
*/
suspend fun typingNotice(isTyping: Boolean): Result<Unit>
/**
* Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters.
* @param widgetSettings The widget settings to use.
* @param clientId The client id to use. It should be unique per app install.
* @param languageTag The language tag to use. If null, the default language will be used.
* @param theme The theme to use. If null, the default theme will be used.
* @return The resulting url, or a failure.
*/
suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings,
clientId: String,
languageTag: String?,
theme: String?,
): Result<String>
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
/**
* Get a [MatrixWidgetDriver] for the provided [widgetSettings].
* @param widgetSettings The widget settings to use.
* @return The resulting [MatrixWidgetDriver], or a failure.
*/
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
/**
* Get the permalink for the room.
*/
suspend fun getPermalink(): Result<String>
suspend fun cancelSend(transactionId: TransactionId): Result<Unit>
/**
* Get the permalink for the provided [eventId].
* @param eventId The event id to get the permalink for.
* @return The permalink, or a failure.
*/
suspend fun getPermalinkFor(eventId: EventId): Result<String>
suspend fun inviteUserById(id: UserId): Result<Unit>
/**
* Send an Element Call started notification if needed.
*/
suspend fun sendCallNotificationIfNeeded(): Result<Unit>
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun setSendQueueEnabled(enabled: Boolean)
suspend fun removeAvatar(): Result<Unit>
/**
* Store the given `ComposerDraft` in the state store of this room.
*/
suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result<Unit>
/**
* Retrieve the `ComposerDraft` stored in the state store for this room.
*/
suspend fun loadComposerDraft(): Result<ComposerDraft?>
/**
* Clear the `ComposerDraft` stored in the state store for this room.
*/
suspend fun clearComposerDraft(): Result<Unit>
/**
* Ignore the local trust for the given devices and resend messages that failed to send because said devices are unverified.
*
* @param devices The map of users identifiers to device identifiers received in the error
* @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload.
*
*/
suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, sendHandle: SendHandle): Result<Unit>
/**
* Remove verification requirements for the given users and
* resend messages that failed to send because their identities were no longer verified.
*
* @param userIds The list of users identifiers received in the error.
* @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload.
*
*/
suspend fun withdrawVerificationAndResend(userIds: List<UserId>, sendHandle: SendHandle): Result<Unit>
override fun close() = destroy()
suspend fun updateRoomNotificationSettings(): Result<Unit>
/**
* Update the canonical alias of the room.
@ -418,12 +226,6 @@ interface MatrixRoom : Closeable {
*/
suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result<Unit>
/**
* Returns the visibility for this room in the room directory.
* If the room is not published, the result will be [RoomVisibility.Private].
*/
suspend fun getRoomVisibility(): Result<RoomVisibility>
/**
* Publish a new room alias for this room in the room directory.
*
@ -454,5 +256,69 @@ interface MatrixRoom : Closeable {
*/
suspend fun updateJoinRule(joinRule: JoinRule): Result<Unit>
suspend fun getUpdatedIsEncrypted(): Result<Boolean>
suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit>
suspend fun updatePowerLevels(roomPowerLevels: RoomPowerLevels): Result<Unit>
suspend fun resetPowerLevels(): Result<RoomPowerLevels>
suspend fun setName(name: String): Result<Unit>
suspend fun setTopic(topic: String): Result<Unit>
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
suspend fun kickUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun banUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun unbanUser(userId: UserId, reason: String? = null): Result<Unit>
/**
* Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters.
* @param widgetSettings The widget settings to use.
* @param clientId The client id to use. It should be unique per app install.
* @param languageTag The language tag to use. If null, the default language will be used.
* @param theme The theme to use. If null, the default theme will be used.
* @return The resulting url, or a failure.
*/
suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings,
clientId: String,
languageTag: String?,
theme: String?,
): Result<String>
/**
* Get a [MatrixWidgetDriver] for the provided [widgetSettings].
* @param widgetSettings The widget settings to use.
* @return The resulting [MatrixWidgetDriver], or a failure.
*/
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
/**
* Send an Element Call started notification if needed.
*/
suspend fun sendCallNotificationIfNeeded(): Result<Unit>
suspend fun setSendQueueEnabled(enabled: Boolean)
/**
* Ignore the local trust for the given devices and resend messages that failed to send because said devices are unverified.
*
* @param devices The map of users identifiers to device identifiers received in the error
* @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload.
*
*/
suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, sendHandle: SendHandle): Result<Unit>
/**
* Remove verification requirements for the given users and
* resend messages that failed to send because their identities were no longer verified.
*
* @param userIds The list of users identifiers received in the error.
* @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload.
*
*/
suspend fun withdrawVerificationAndResend(userIds: List<UserId>, sendHandle: SendHandle): Result<Unit>
}

View file

@ -1,24 +0,0 @@
/*
* Copyright 2023, 2024 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
sealed interface MatrixRoomNotificationSettingsState {
data object Unknown : MatrixRoomNotificationSettingsState
data class Pending(val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState
data class Error(val failure: Throwable, val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState
data class Ready(val roomNotificationSettings: RoomNotificationSettings) : MatrixRoomNotificationSettingsState
}
fun MatrixRoomNotificationSettingsState.roomNotificationSettings(): RoomNotificationSettings? {
return when (this) {
is MatrixRoomNotificationSettingsState.Ready -> roomNotificationSettings
is MatrixRoomNotificationSettingsState.Pending -> prevRoomNotificationSettings
is MatrixRoomNotificationSettingsState.Error -> prevRoomNotificationSettings
else -> null
}
}

View file

@ -7,21 +7,12 @@
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
/** A reference to a room either invited, knocked or banned. */
interface RoomPreview : AutoCloseable {
val sessionId: SessionId
val info: RoomPreviewInfo
/** Leave the room ie.decline invite or cancel knock. */
suspend fun leave(): Result<Unit>
/**
* Forget the room if we had access to it, and it was left or banned.
*/
suspend fun forget(): Result<Unit>
interface NotJoinedRoom : AutoCloseable {
val previewInfo: RoomPreviewInfo
val localRoom: BaseRoom?
/**
* Get the membership details of the user in the room, as well as from the user who sent the `m.room.member` event.

View file

@ -19,7 +19,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@Immutable
data class MatrixRoomInfo(
data class RoomInfo(
val id: RoomId,
/** The room's name from the room state event if received from sync, or one that's been computed otherwise. */
val name: String?,

View file

@ -21,11 +21,11 @@ fun isDm(isDirect: Boolean, activeMembersCount: Int): Boolean {
}
/**
* Returns whether the [MatrixRoom] is a DM, with an updated state from the latest [MatrixRoomInfo].
* Returns whether the [BaseRoom] is a DM, with an updated state from the latest [RoomInfo].
*/
suspend fun MatrixRoom.isDm() = roomInfoFlow.first().isDm
suspend fun BaseRoom.isDm() = roomInfoFlow.first().isDm
/**
* Returns whether the [MatrixRoomInfo] is from a DM.
* Returns whether the [RoomInfo] is from a DM.
*/
val MatrixRoomInfo.isDm get() = isDm(isDirect, activeMembersCount.toInt())
val RoomInfo.isDm get() = isDm(isDirect, activeMembersCount.toInt())

View file

@ -11,26 +11,26 @@ import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
sealed interface MatrixRoomMembersState {
data object Unknown : MatrixRoomMembersState
data class Pending(val prevRoomMembers: ImmutableList<RoomMember>? = null) : MatrixRoomMembersState
data class Error(val failure: Throwable, val prevRoomMembers: ImmutableList<RoomMember>? = null) : MatrixRoomMembersState
data class Ready(val roomMembers: ImmutableList<RoomMember>) : MatrixRoomMembersState
sealed interface RoomMembersState {
data object Unknown : RoomMembersState
data class Pending(val prevRoomMembers: ImmutableList<RoomMember>? = null) : RoomMembersState
data class Error(val failure: Throwable, val prevRoomMembers: ImmutableList<RoomMember>? = null) : RoomMembersState
data class Ready(val roomMembers: ImmutableList<RoomMember>) : RoomMembersState
}
fun MatrixRoomMembersState.roomMembers(): List<RoomMember>? {
fun RoomMembersState.roomMembers(): List<RoomMember>? {
return when (this) {
is MatrixRoomMembersState.Ready -> roomMembers
is MatrixRoomMembersState.Pending -> prevRoomMembers
is MatrixRoomMembersState.Error -> prevRoomMembers
is RoomMembersState.Ready -> roomMembers
is RoomMembersState.Pending -> prevRoomMembers
is RoomMembersState.Error -> prevRoomMembers
else -> null
}
}
fun MatrixRoomMembersState.joinedRoomMembers(): List<RoomMember> {
fun RoomMembersState.joinedRoomMembers(): List<RoomMember> {
return roomMembers().orEmpty().filter { it.membership == RoomMembershipState.JOIN }
}
fun MatrixRoomMembersState.activeRoomMembers(): List<RoomMember> {
fun RoomMembersState.activeRoomMembers(): List<RoomMember> {
return roomMembers().orEmpty().filter { it.membership.isActive() }
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2023, 2024 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
sealed interface RoomNotificationSettingsState {
data object Unknown : RoomNotificationSettingsState
data class Pending(val prevRoomNotificationSettings: RoomNotificationSettings? = null) : RoomNotificationSettingsState
data class Error(val failure: Throwable, val prevRoomNotificationSettings: RoomNotificationSettings? = null) : RoomNotificationSettingsState
data class Ready(val roomNotificationSettings: RoomNotificationSettings) : RoomNotificationSettingsState
}
fun RoomNotificationSettingsState.roomNotificationSettings(): RoomNotificationSettings? {
return when (this) {
is RoomNotificationSettingsState.Ready -> roomNotificationSettings
is RoomNotificationSettingsState.Pending -> prevRoomNotificationSettings
is RoomNotificationSettingsState.Error -> prevRoomNotificationSettings
else -> null
}
}

View file

@ -8,12 +8,12 @@
package io.element.android.libraries.matrix.api.room.alias
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
/**
* Return true if the given roomIdOrAlias is the same room as this room.
*/
fun MatrixRoom.matches(roomIdOrAlias: RoomIdOrAlias): Boolean {
fun BaseRoom.matches(roomIdOrAlias: RoomIdOrAlias): Boolean {
return when (roomIdOrAlias) {
is RoomIdOrAlias.Id -> {
roomIdOrAlias.roomId == roomId

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.matrix.api.room.powerlevels
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.activeRoomMembers
import kotlinx.collections.immutable.ImmutableList
@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.map
/**
* Return a flow of the list of active room members who have the given role.
*/
fun MatrixRoom.usersWithRole(role: RoomMember.Role): Flow<ImmutableList<RoomMember>> {
fun BaseRoom.usersWithRole(role: RoomMember.Role): Flow<ImmutableList<RoomMember>> {
return roomInfoFlow
.map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } }
.combine(membersStateFlow) { powerLevels, membersState ->

View file

@ -1,70 +0,0 @@
/*
* Copyright 2023, 2024 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.powerlevels
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType
data class MatrixRoomPowerLevels(
val ban: Long,
val invite: Long,
val kick: Long,
val sendEvents: Long,
val redactEvents: Long,
val roomName: Long,
val roomAvatar: Long,
val roomTopic: Long,
)
/**
* Shortcut for calling [MatrixRoom.canUserInvite] with our own user.
*/
suspend fun MatrixRoom.canInvite(): Result<Boolean> = canUserInvite(sessionId)
/**
* Shortcut for calling [MatrixRoom.canUserKick] with our own user.
*/
suspend fun MatrixRoom.canKick(): Result<Boolean> = canUserKick(sessionId)
/**
* Shortcut for calling [MatrixRoom.canUserBan] with our own user.
*/
suspend fun MatrixRoom.canBan(): Result<Boolean> = canUserBan(sessionId)
/**
* Shortcut for calling [MatrixRoom.canUserSendState] with our own user.
*/
suspend fun MatrixRoom.canSendState(type: StateEventType): Result<Boolean> = canUserSendState(sessionId, type)
/**
* Shortcut for calling [MatrixRoom.canUserSendMessage] with our own user.
*/
suspend fun MatrixRoom.canSendMessage(type: MessageEventType): Result<Boolean> = canUserSendMessage(sessionId, type)
/**
* Shortcut for calling [MatrixRoom.canUserRedactOwn] with our own user.
*/
suspend fun MatrixRoom.canRedactOwn(): Result<Boolean> = canUserRedactOwn(sessionId)
/**
* Shortcut for calling [MatrixRoom.canRedactOther] with our own user.
*/
suspend fun MatrixRoom.canRedactOther(): Result<Boolean> = canUserRedactOther(sessionId)
/**
* Shortcut for checking if current user can handle knock requests.
*/
suspend fun MatrixRoom.canHandleKnockRequests(): Result<Boolean> = runCatching {
canInvite().getOrThrow() || canBan().getOrThrow() || canKick().getOrThrow()
}
/**
* Shortcut for calling [MatrixRoom.canUserPinUnpin] with our own user.
*/
suspend fun MatrixRoom.canPinUnpin(): Result<Boolean> = canUserPinUnpin(sessionId)

View file

@ -0,0 +1,70 @@
/*
* Copyright 2023, 2024 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.powerlevels
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType
data class RoomPowerLevels(
val ban: Long,
val invite: Long,
val kick: Long,
val sendEvents: Long,
val redactEvents: Long,
val roomName: Long,
val roomAvatar: Long,
val roomTopic: Long,
)
/**
* Shortcut for calling [BaseRoom.canUserInvite] with our own user.
*/
suspend fun BaseRoom.canInvite(): Result<Boolean> = canUserInvite(sessionId)
/**
* Shortcut for calling [BaseRoom.canUserKick] with our own user.
*/
suspend fun BaseRoom.canKick(): Result<Boolean> = canUserKick(sessionId)
/**
* Shortcut for calling [BaseRoom.canUserBan] with our own user.
*/
suspend fun BaseRoom.canBan(): Result<Boolean> = canUserBan(sessionId)
/**
* Shortcut for calling [BaseRoom.canUserSendState] with our own user.
*/
suspend fun BaseRoom.canSendState(type: StateEventType): Result<Boolean> = canUserSendState(sessionId, type)
/**
* Shortcut for calling [BaseRoom.canUserSendMessage] with our own user.
*/
suspend fun BaseRoom.canSendMessage(type: MessageEventType): Result<Boolean> = canUserSendMessage(sessionId, type)
/**
* Shortcut for calling [BaseRoom.canUserRedactOwn] with our own user.
*/
suspend fun BaseRoom.canRedactOwn(): Result<Boolean> = canUserRedactOwn(sessionId)
/**
* Shortcut for calling [BaseRoom.canRedactOther] with our own user.
*/
suspend fun BaseRoom.canRedactOther(): Result<Boolean> = canUserRedactOther(sessionId)
/**
* Shortcut for checking if current user can handle knock requests.
*/
suspend fun BaseRoom.canHandleKnockRequests(): Result<Boolean> = runCatching {
canInvite().getOrThrow() || canBan().getOrThrow() || canKick().getOrThrow()
}
/**
* Shortcut for calling [BaseRoom.canUserPinUnpin] with our own user.
*/
suspend fun BaseRoom.canPinUnpin(): Result<Boolean> = canUserPinUnpin(sessionId)

View file

@ -10,8 +10,8 @@ package io.element.android.libraries.matrix.api.room.recent
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
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.toMatrixUser
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -52,6 +52,6 @@ suspend fun MatrixClient.getRecentDirectRooms(
return result
}
suspend fun MatrixRoom.isJoined(): Boolean {
suspend fun BaseRoom.isJoined(): Boolean {
return roomInfoFlow.first().currentUserMembership == CurrentUserMembership.JOINED
}

View file

@ -7,11 +7,11 @@
package io.element.android.libraries.matrix.api.roomlist
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.message.RoomMessage
data class RoomSummary(
val info: MatrixRoomInfo,
val info: RoomInfo,
val lastMessage: RoomMessage?,
) {
val roomId = info.id

View file

@ -30,11 +30,12 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.NotJoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomPreview
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
@ -55,13 +56,15 @@ import io.element.android.libraries.matrix.impl.notification.RustNotificationSer
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.oidc.toRustAction
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.GetRoomResult
import io.element.android.libraries.matrix.impl.room.NotJoinedRustRoom
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustRoomFactory
import io.element.android.libraries.matrix.impl.room.RustRoomPreview
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory
import io.element.android.libraries.matrix.impl.room.history.map
import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
import io.element.android.libraries.matrix.impl.roomdirectory.map
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
@ -186,6 +189,7 @@ class RustMatrixClient(
private val roomMembershipObserver = RoomMembershipObserver()
private val roomFactory = RustRoomFactory(
innerClient = innerClient,
roomListService = roomListService,
innerRoomListService = innerRoomListService,
sessionId = sessionId,
@ -263,12 +267,17 @@ class RustMatrixClient(
}
}
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
return roomFactory.create(roomId)
override suspend fun getRoom(roomId: RoomId): BaseRoom? {
return roomFactory.getBaseRoom(roomId)
}
override suspend fun getPendingRoom(roomId: RoomId): RoomPreview? {
return roomFactory.createRoomPreview(roomId)
override suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? {
return try {
(roomFactory.getJoinedRoomOrPreview(roomId) as GetRoomResult.Joined).joinedRoom
} catch (e: ClassCastException) {
Timber.e(e, "Room $roomId is not a joined room")
null
}
}
/**
@ -455,13 +464,29 @@ class RustMatrixClient(
}
}
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomPreview> = withContext(sessionDispatcher) {
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<NotJoinedRoom> = withContext(sessionDispatcher) {
runCatching {
val roomPreview = when (roomIdOrAlias) {
is RoomIdOrAlias.Alias -> innerClient.getRoomPreviewFromRoomAlias(roomIdOrAlias.roomAlias.value)
is RoomIdOrAlias.Id -> innerClient.getRoomPreviewFromRoomId(roomIdOrAlias.roomId.value, serverNames)
when (roomIdOrAlias) {
is RoomIdOrAlias.Alias -> {
val roomId = innerClient.resolveRoomAlias(roomIdOrAlias.roomAlias.value)?.roomId?.let { RoomId(it) }
var room = (roomId?.let { roomFactory.getJoinedRoomOrPreview(it) } as? GetRoomResult.NotJoined)?.notJoinedRoom
if (room == null) {
val preview = innerClient.getRoomPreviewFromRoomAlias(roomIdOrAlias.roomAlias.value)
room = NotJoinedRustRoom(sessionId, null, RoomPreviewInfoMapper.map(preview.info()))
}
room
}
is RoomIdOrAlias.Id -> {
var room = (roomFactory.getJoinedRoomOrPreview(roomIdOrAlias.roomId) as? GetRoomResult.NotJoined)?.notJoinedRoom
if (room == null) {
val preview = innerClient.getRoomPreviewFromRoomId(roomIdOrAlias.roomId.value, serverNames)
room = NotJoinedRustRoom(sessionId, null, RoomPreviewInfoMapper.map(preview.info()))
}
room
}
}
RustRoomPreview(sessionId, roomPreview, roomMembershipObserver)
}.mapFailure { it.mapClientException() }
}

View file

@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.impl.analytics
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.isDm
import kotlinx.coroutines.flow.first
@ -24,7 +24,7 @@ private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize {
}
}
suspend fun MatrixRoom.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom {
suspend fun BaseRoom.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom {
val roomInfo = roomInfoFlow.first()
return JoinedRoom(
isDM = roomInfo.isDm,

View file

@ -1,5 +1,5 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* 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.
@ -17,7 +17,6 @@ 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.SessionId
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
@ -28,55 +27,43 @@ 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
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState
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.MatrixRoomPowerLevels
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.ReceiptType
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
import io.element.android.libraries.matrix.impl.mapper.map
import io.element.android.libraries.matrix.impl.room.draft.into
import io.element.android.libraries.matrix.impl.room.history.map
import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
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.RustTimeline
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
import io.element.android.libraries.matrix.impl.util.MessageEventContent
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.CoroutineScope
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.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
@ -89,7 +76,6 @@ 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.RoomInfo
import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
import org.matrix.rustcomponents.sdk.TimelineConfiguration
@ -103,40 +89,36 @@ import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk.RoomPowerLevelChanges
import uniffi.matrix_sdk_base.EncryptionState
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.Room as InnerRoom
import org.matrix.rustcomponents.sdk.RoomInfo as InnerRoomInfo
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
@Suppress("LargeClass")
class RustMatrixRoom(
override val sessionId: SessionId,
private val deviceId: DeviceId,
private val innerRoom: InnerRoom,
innerTimeline: InnerTimeline,
class JoinedRustRoom(
private val baseRoom: RustBaseRoom,
private val liveInnerTimeline: InnerTimeline,
private val notificationSettingsService: NotificationSettingsService,
sessionCoroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
private val roomInfoMapper: RoomInfoMapper,
private val systemClock: SystemClock,
private val roomContentForwarder: RoomContentForwarder,
private val roomSyncSubscriber: RoomSyncSubscriber,
private val matrixRoomInfoMapper: MatrixRoomInfoMapper,
private val featureFlagService: FeatureFlagService,
private val roomMembershipObserver: RoomMembershipObserver,
initialRoomInfo: MatrixRoomInfo,
) : MatrixRoom {
override val roomId = RoomId(innerRoom.id())
) : JoinedRoom, BaseRoom by baseRoom {
// Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)
private val innerRoom = baseRoom.innerRoom
override val roomInfoFlow: StateFlow<MatrixRoomInfo> = mxCallbackFlow {
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: RoomInfo) {
channel.trySend(matrixRoomInfoMapper.map(roomInfo))
override fun call(roomInfo: InnerRoomInfo) {
channel.trySend(roomInfoMapper.map(roomInfo))
}
})
}.stateIn(sessionCoroutineScope, started = SharingStarted.Lazily, initialValue = initialRoomInfo)
}.stateIn(roomCoroutineScope, started = SharingStarted.Lazily, initialValue = baseRoom.info())
override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow {
val initial = emptyList<UserId>()
@ -178,27 +160,12 @@ class RustMatrixRoom(
})
}
// Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)
override val roomNotificationSettingsStateFlow = MutableStateFlow<RoomNotificationSettingsState>(RoomNotificationSettingsState.Unknown)
// ...except getMember methods as it could quickly fill the roomDispatcher...
private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8)
override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
private val _syncUpdateFlow = MutableStateFlow(0L)
private val roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher)
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
override val liveTimeline = createTimeline(innerTimeline, mode = Timeline.Mode.LIVE) {
_syncUpdateFlow.value = systemClock.epochMillis()
override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.LIVE) {
syncUpdateFlow.value = systemClock.epochMillis()
}
override val membersStateFlow: StateFlow<MatrixRoomMembersState> = roomMemberListFetcher.membersFlow
override val syncUpdateFlow: StateFlow<Long> = _syncUpdateFlow.asStateFlow()
init {
val powerLevelChanges = roomInfoFlow.map { it.userPowerLevels }.distinctUntilChanged()
val membershipChanges = liveTimeline.membershipChangeEventReceived.onStart { emit(Unit) }
@ -206,12 +173,13 @@ class RustMatrixRoom(
// Skip initial one
.drop(1)
// The new events should already be in the SDK cache, no need to fetch them from the server
.onEach { roomMemberListFetcher.fetchRoomMembers(source = RoomMemberListFetcher.Source.CACHE) }
.onEach { baseRoom.roomMemberListFetcher.fetchRoomMembers(source = RoomMemberListFetcher.Source.CACHE) }
.launchIn(roomCoroutineScope)
.invokeOnCompletion {
Timber.d("Observing membership changes for room $roomId stopped, reason: $it")
}
}
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
override suspend fun createTimeline(
createTimelineParams: CreateTimelineParams,
): Result<Timeline> = withContext(roomDispatcher) {
@ -273,17 +241,14 @@ class RustMatrixRoom(
dateDividerMode = dateDividerMode,
trackReadReceipts = trackReadReceipts,
)
).let { inner ->
).let { innerTimeline ->
val mode = when (createTimelineParams) {
is CreateTimelineParams.Focused -> Timeline.Mode.FOCUSED_ON_EVENT
is CreateTimelineParams.MediaOnly -> Timeline.Mode.MEDIA
is CreateTimelineParams.MediaOnlyFocused -> Timeline.Mode.FOCUSED_ON_EVENT
CreateTimelineParams.PinnedOnly -> Timeline.Mode.PINNED_EVENTS
}
createTimeline(
timeline = inner,
mode = mode,
)
innerTimeline.map(mode = mode)
}
}.mapFailure {
when (createTimelineParams) {
@ -299,105 +264,8 @@ class RustMatrixRoom(
}
}
override fun destroy() {
roomCoroutineScope.cancel()
liveTimeline.close()
}
override suspend fun updateMembers() {
val useCache = membersStateFlow.value is MatrixRoomMembersState.Unknown
val source = if (useCache) {
RoomMemberListFetcher.Source.CACHE_AND_SERVER
} else {
RoomMemberListFetcher.Source.SERVER
}
roomMemberListFetcher.fetchRoomMembers(source = source)
}
override suspend fun getMembers(limit: Int) = withContext(roomDispatcher) {
runCatching {
innerRoom.members().use {
it.nextChunk(limit.toUInt()).orEmpty().map { roomMember ->
RoomMemberMapper.map(roomMember)
}
}
}
}
override suspend fun getUpdatedMember(userId: UserId): Result<RoomMember> = withContext(roomDispatcher) {
runCatching {
RoomMemberMapper.map(innerRoom.member(userId.value))
}
}
override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) {
runCatching {
innerRoom.memberDisplayName(userId.value)
}
}
override suspend fun updateRoomNotificationSettings(): Result<Unit> = withContext(roomDispatcher) {
val currentState = _roomNotificationSettingsStateFlow.value
val currentRoomNotificationSettings = currentState.roomNotificationSettings()
_roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings)
runCatching {
val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow()
notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow()
}.map {
_roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(it)
}.onFailure {
_roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Error(
prevRoomNotificationSettings = currentRoomNotificationSettings,
failure = it
)
}
}
override suspend fun userRole(userId: UserId): Result<RoomMember.Role> = withContext(roomDispatcher) {
runCatching {
RoomMemberMapper.mapRole(innerRoom.suggestedRoleForUser(userId.value))
}
}
override suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit> {
return runCatching {
val powerLevelChanges = changes.map { UserPowerLevelUpdate(it.userId.value, it.powerLevel) }
innerRoom.updatePowerLevelsForUsers(powerLevelChanges)
}
}
override suspend fun powerLevels(): Result<MatrixRoomPowerLevels> = withContext(roomDispatcher) {
runCatching {
RoomPowerLevelsMapper.map(innerRoom.getPowerLevels())
}
}
override suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result<Unit> = withContext(roomDispatcher) {
runCatching {
val changes = RoomPowerLevelChanges(
ban = matrixRoomPowerLevels.ban,
invite = matrixRoomPowerLevels.invite,
kick = matrixRoomPowerLevels.kick,
redact = matrixRoomPowerLevels.redactEvents,
eventsDefault = matrixRoomPowerLevels.sendEvents,
roomName = matrixRoomPowerLevels.roomName,
roomAvatar = matrixRoomPowerLevels.roomAvatar,
roomTopic = matrixRoomPowerLevels.roomTopic,
)
innerRoom.applyPowerLevelChanges(changes)
}
}
override suspend fun resetPowerLevels(): Result<MatrixRoomPowerLevels> = withContext(roomDispatcher) {
runCatching {
RoomPowerLevelsMapper.map(innerRoom.resetPowerLevels())
}
}
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = withContext(roomDispatcher) {
runCatching {
innerRoom.memberAvatarUrl(userId.value)
}
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit> {
return liveTimeline.sendMessage(body, htmlBody, intentionalMentions)
}
override suspend fun editMessage(
@ -413,84 +281,6 @@ class RustMatrixRoom(
}
}
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit> {
return liveTimeline.sendMessage(body, htmlBody, intentionalMentions)
}
override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.leave()
}.onSuccess {
roomMembershipObserver.notifyUserLeftRoom(roomId)
}
}
override suspend fun join(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.join()
}
}
override suspend fun inviteUserById(id: UserId): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.inviteUserById(id.value)
}
}
override suspend fun canUserInvite(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserInvite(userId.value)
}
}
override suspend fun canUserKick(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserKick(userId.value)
}
}
override suspend fun canUserBan(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserBan(userId.value)
}
}
override suspend fun canUserRedactOwn(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserRedactOwn(userId.value)
}
}
override suspend fun canUserRedactOther(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserRedactOther(userId.value)
}
}
override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserSendState(userId.value, type.map())
}
}
override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserSendMessage(userId.value, type.map())
}
}
override suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserTriggerRoomNotification(userId.value)
}
}
override suspend fun canUserPinUnpin(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserPinUnpin(userId.value)
}
}
override suspend fun sendImage(
file: File,
thumbnailFile: File?,
@ -593,93 +383,6 @@ class RustMatrixRoom(
return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType)
}
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 updateAvatar(mimeType: String, data: ByteArray): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.uploadAvatar(mimeType, data, null)
}
}
override suspend fun removeAvatar(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.removeAvatar()
}
}
override suspend fun setName(name: String): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.setName(name)
}
}
override suspend fun setTopic(topic: String): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.setTopic(topic)
}
}
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)
if (blockUserId != null) {
innerRoom.ignoreUser(blockUserId.value)
}
}
}
override suspend fun clearEventCacheStorage(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.clearEventCacheStorage()
}
}
override suspend fun kickUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.kickUser(userId.value, reason)
}
}
override suspend fun banUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.banUser(userId.value, reason)
}
}
override suspend fun unbanUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.unbanUser(userId.value, reason)
}
}
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.setIsFavourite(isFavorite, null)
}
}
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.markAsRead(receiptType.toRustReceiptType())
}
}
override suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.setUnreadFlag(isUnread)
}
}
override suspend fun createPoll(
question: String,
answers: List<String>,
@ -719,95 +422,70 @@ class RustMatrixRoom(
}
}
override suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings,
clientId: String,
languageTag: String?,
theme: String?,
) = withContext(roomDispatcher) {
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 {
widgetSettings.generateWidgetWebViewUrl(innerRoom, clientId, languageTag, theme)
innerRoom.inviteUserById(id.value)
}
}
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> {
return runCatching {
RustWidgetDriver(
widgetSettings = widgetSettings,
room = innerRoom,
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
return getElementCallRequiredPermissions(sessionId.value, deviceId.value)
}
},
)
}
}
override suspend fun getPermalink(): Result<String> = withContext(roomDispatcher) {
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.matrixToPermalink()
innerRoom.uploadAvatar(mimeType, data, null)
}
}
override suspend fun getPermalinkFor(eventId: EventId): Result<String> = withContext(roomDispatcher) {
override suspend fun removeAvatar(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.matrixToEventPermalink(eventId.value)
innerRoom.removeAvatar()
}
}
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> = withContext(roomDispatcher) {
override suspend fun setName(name: String): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.sendCallNotificationIfNeeded()
innerRoom.setName(name)
}
}
override suspend fun setSendQueueEnabled(enabled: Boolean) {
withContext(roomDispatcher) {
Timber.d("setSendQueuesEnabled: $enabled")
runCatching {
innerRoom.enableSendQueue(enabled)
override suspend fun setTopic(topic: String): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.setTopic(topic)
}
}
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)
if (blockUserId != null) {
innerRoom.ignoreUser(blockUserId.value)
}
}
}
override suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result<Unit> = withContext(roomDispatcher) {
override suspend fun updateRoomNotificationSettings(): Result<Unit> = withContext(roomDispatcher) {
val currentState = roomNotificationSettingsStateFlow.value
val currentRoomNotificationSettings = currentState.roomNotificationSettings()
roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings)
runCatching {
Timber.d("saveComposerDraft: $composerDraft into $roomId")
innerRoom.saveComposerDraft(composerDraft.into())
}
}
override suspend fun loadComposerDraft(): Result<ComposerDraft?> = withContext(roomDispatcher) {
runCatching {
Timber.d("loadComposerDraft for $roomId")
innerRoom.loadComposerDraft()?.into()
}
}
override suspend fun clearComposerDraft(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
Timber.d("clearComposerDraft for $roomId")
innerRoom.clearComposerDraft()
}
}
override suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, sendHandle: SendHandle) = withContext(roomDispatcher) {
runCatching {
innerRoom.ignoreDeviceTrustAndResend(
devices = devices.entries.associate { entry ->
entry.key.value to entry.value.map { it.value }
},
sendHandle = (sendHandle as RustSendHandle).inner,
)
}
}
override suspend fun withdrawVerificationAndResend(userIds: List<UserId>, sendHandle: SendHandle) = withContext(roomDispatcher) {
runCatching {
innerRoom.withdrawVerificationAndResend(
userIds = userIds.map { it.value },
sendHandle = (sendHandle as RustSendHandle).inner,
val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow()
notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow()
}.map {
roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Ready(it)
}.onFailure {
roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Error(
prevRoomNotificationSettings = currentRoomNotificationSettings,
failure = it
)
}
}
@ -842,12 +520,6 @@ class RustMatrixRoom(
}
}
override suspend fun getRoomVisibility(): Result<RoomVisibility> = withContext(roomDispatcher) {
runCatching {
innerRoom.getRoomVisibility().map()
}
}
override suspend fun enableEncryption(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.enableEncryption()
@ -860,22 +532,128 @@ class RustMatrixRoom(
}
}
override suspend fun getUpdatedIsEncrypted(): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.latestEncryptionState() == EncryptionState.ENCRYPTED
override suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit> {
return runCatching {
val powerLevelChanges = changes.map { UserPowerLevelUpdate(it.userId.value, it.powerLevel) }
innerRoom.updatePowerLevelsForUsers(powerLevelChanges)
}
}
private fun createTimeline(
timeline: InnerTimeline,
override suspend fun updatePowerLevels(roomPowerLevels: RoomPowerLevels): Result<Unit> = withContext(roomDispatcher) {
runCatching {
val changes = RoomPowerLevelChanges(
ban = roomPowerLevels.ban,
invite = roomPowerLevels.invite,
kick = roomPowerLevels.kick,
redact = roomPowerLevels.redactEvents,
eventsDefault = roomPowerLevels.sendEvents,
roomName = roomPowerLevels.roomName,
roomAvatar = roomPowerLevels.roomAvatar,
roomTopic = roomPowerLevels.roomTopic,
)
innerRoom.applyPowerLevelChanges(changes)
}
}
override suspend fun resetPowerLevels(): Result<RoomPowerLevels> = withContext(roomDispatcher) {
runCatching {
RoomPowerLevelsMapper.map(innerRoom.resetPowerLevels())
}
}
override suspend fun kickUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.kickUser(userId.value, reason)
}
}
override suspend fun banUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.banUser(userId.value, reason)
}
}
override suspend fun unbanUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.unbanUser(userId.value, reason)
}
}
override suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings,
clientId: String,
languageTag: String?,
theme: String?,
) = withContext(roomDispatcher) {
runCatching {
widgetSettings.generateWidgetWebViewUrl(innerRoom, clientId, languageTag, theme)
}
}
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> {
return runCatching {
RustWidgetDriver(
widgetSettings = widgetSettings,
room = innerRoom,
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
return getElementCallRequiredPermissions(sessionId.value, baseRoom.deviceId.value)
}
},
)
}
}
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.sendCallNotificationIfNeeded()
}
}
override suspend fun setSendQueueEnabled(enabled: Boolean) {
withContext(roomDispatcher) {
Timber.d("setSendQueuesEnabled: $enabled")
runCatching {
innerRoom.enableSendQueue(enabled)
}
}
}
override suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, sendHandle: SendHandle) = withContext(roomDispatcher) {
runCatching {
innerRoom.ignoreDeviceTrustAndResend(
devices = devices.entries.associate { entry ->
entry.key.value to entry.value.map { it.value }
},
sendHandle = (sendHandle as RustSendHandle).inner,
)
}
}
override suspend fun withdrawVerificationAndResend(userIds: List<UserId>, sendHandle: SendHandle) = withContext(roomDispatcher) {
runCatching {
innerRoom.withdrawVerificationAndResend(
userIds = userIds.map { it.value },
sendHandle = (sendHandle as RustSendHandle).inner,
)
}
}
override fun destroy() {
baseRoom.destroy()
liveInnerTimeline.close()
roomCoroutineScope.cancel()
}
private fun InnerTimeline.map(
mode: Timeline.Mode,
onNewSyncedEvent: () -> Unit = {},
): Timeline {
val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$timeline")
val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$this")
return RustTimeline(
mode = mode,
matrixRoom = this,
inner = timeline,
joinedRoom = this@JoinedRustRoom,
inner = this@map,
systemClock = systemClock,
coroutineScope = timelineCoroutineScope,
dispatcher = roomDispatcher,

View file

@ -0,0 +1,35 @@
/*
* Copyright 2024 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 androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.NotJoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
@Immutable
class NotJoinedRustRoom(
private val sessionId: SessionId,
override val localRoom: RustBaseRoom?,
override val previewInfo: RoomPreviewInfo,
) : NotJoinedRoom {
override suspend fun membershipDetails(): Result<RoomMembershipDetails?> = runCatching {
val room = localRoom?.innerRoom ?: return@runCatching null
val (ownMember, senderInfo) = room.memberWithSenderInfo(sessionId.value)
RoomMembershipDetails(
currentUserMember = RoomMemberMapper.map(ownMember),
senderMember = senderInfo?.let { RoomMemberMapper.map(it) },
)
}
override fun close() {
localRoom?.close()
}
}

View file

@ -12,7 +12,7 @@ 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.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.room.history.map
@ -28,9 +28,9 @@ import org.matrix.rustcomponents.sdk.Membership as RustMembership
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
class MatrixRoomInfoMapper {
fun map(rustRoomInfo: RustRoomInfo): MatrixRoomInfo = rustRoomInfo.let {
return MatrixRoomInfo(
class RoomInfoMapper {
fun map(rustRoomInfo: RustRoomInfo): RoomInfo = rustRoomInfo.let {
return RoomInfo(
id = RoomId(it.id),
creator = it.creator?.let(::UserId),
name = it.displayName,
@ -70,44 +70,6 @@ class MatrixRoomInfoMapper {
historyVisibility = it.historyVisibility.map(),
)
}
// fun map(rustRoom: Room): MatrixRoomInfo = with(rustRoom) {
// return MatrixRoomInfo(
// id = RoomId(id()),
// name = rawName(),
// rawName = displayName(),
// topic = topic(),
// avatarUrl = avatarUrl(),
// isPublic = isPublic(),
// isDirect = null,
// isEncrypted = encryptionState() == EncryptionState.ENCRYPTED,
// joinRule = null,
// isSpace = isSpace(),
// isTombstoned = isTombstoned(),
// isFavorite = null,
// canonicalAlias = canonicalAlias()?.let(::RoomAlias),
// alternativeAliases = alternativeAliases().map(::RoomAlias).toImmutableList(),
// currentUserMembership = membership().map(),
// inviter = null,
// activeMembersCount = activeMembersCount().toLong(),
// invitedMembersCount = invitedMembersCount().toLong(),
// joinedMembersCount = joinedMembersCount().toLong(),
// userPowerLevels = persistentMapOf(),
// highlightCount = 0,
// notificationCount = 0,
// userDefinedNotificationMode = null,
// hasRoomCall = hasActiveRoomCall(),
// activeRoomCallParticipants = activeRoomCallParticipants().map(::UserId).toImmutableList(),
// isMarkedUnread = false,
// numUnreadMessages = 0,
// numUnreadNotifications = 0,
// numUnreadMentions = 0,
// heroes = heroes().map(RoomHero::map).toImmutableList(),
// pinnedEventIds = persistentListOf(),
// creator = null,
// historyVisibility = null,
// )
// }
}
fun RustMembership.map(): CurrentUserMembership = when (this) {

View file

@ -0,0 +1,267 @@
/*
* Copyright 2023, 2024 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 io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.impl.room.draft.into
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk_base.EncryptionState
import org.matrix.rustcomponents.sdk.Room as InnerRoom
class RustBaseRoom(
override val sessionId: SessionId,
internal val deviceId: DeviceId,
internal val innerRoom: InnerRoom,
coroutineDispatchers: CoroutineDispatchers,
private val roomSyncSubscriber: RoomSyncSubscriber,
private val roomMembershipObserver: RoomMembershipObserver,
sessionCoroutineScope: CoroutineScope,
initialRoomInfo: RoomInfo,
) : BaseRoom {
override val roomId = RoomId(innerRoom.id())
// Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)
// ...except getMember methods as it could quickly fill the roomDispatcher...
private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8)
internal val roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher)
override val membersStateFlow: StateFlow<RoomMembersState> = roomMemberListFetcher.membersFlow
override val roomInfoFlow: StateFlow<RoomInfo> = MutableStateFlow(initialRoomInfo)
override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
override suspend fun updateMembers() {
val useCache = membersStateFlow.value is RoomMembersState.Unknown
val source = if (useCache) {
RoomMemberListFetcher.Source.CACHE_AND_SERVER
} else {
RoomMemberListFetcher.Source.SERVER
}
roomMemberListFetcher.fetchRoomMembers(source = source)
}
override suspend fun getMembers(limit: Int) = withContext(roomDispatcher) {
runCatching {
innerRoom.members().use {
it.nextChunk(limit.toUInt()).orEmpty().map { roomMember ->
RoomMemberMapper.map(roomMember)
}
}
}
}
override suspend fun getUpdatedMember(userId: UserId): Result<RoomMember> = withContext(roomDispatcher) {
runCatching {
RoomMemberMapper.map(innerRoom.member(userId.value))
}
}
override fun destroy() {
innerRoom.destroy()
}
override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) {
runCatching {
innerRoom.memberDisplayName(userId.value)
}
}
override suspend fun userRole(userId: UserId): Result<RoomMember.Role> = withContext(roomDispatcher) {
runCatching {
RoomMemberMapper.mapRole(innerRoom.suggestedRoleForUser(userId.value))
}
}
override suspend fun powerLevels(): Result<RoomPowerLevels> = withContext(roomDispatcher) {
runCatching {
RoomPowerLevelsMapper.map(innerRoom.getPowerLevels())
}
}
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = withContext(roomDispatcher) {
runCatching {
innerRoom.memberAvatarUrl(userId.value)
}
}
override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.leave()
}.onSuccess {
roomMembershipObserver.notifyUserLeftRoom(roomId)
}
}
override suspend fun join(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.join()
}
}
override suspend fun forget(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.forget()
}
}
override suspend fun canUserInvite(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserInvite(userId.value)
}
}
override suspend fun canUserKick(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserKick(userId.value)
}
}
override suspend fun canUserBan(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserBan(userId.value)
}
}
override suspend fun canUserRedactOwn(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserRedactOwn(userId.value)
}
}
override suspend fun canUserRedactOther(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserRedactOther(userId.value)
}
}
override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserSendState(userId.value, type.map())
}
}
override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserSendMessage(userId.value, type.map())
}
}
override suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserTriggerRoomNotification(userId.value)
}
}
override suspend fun canUserPinUnpin(userId: UserId): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.canUserPinUnpin(userId.value)
}
}
override suspend fun clearEventCacheStorage(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.clearEventCacheStorage()
}
}
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.setIsFavourite(isFavorite, null)
}
}
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.markAsRead(receiptType.toRustReceiptType())
}
}
override suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.setUnreadFlag(isUnread)
}
}
override suspend fun getPermalink(): Result<String> = withContext(roomDispatcher) {
runCatching {
innerRoom.matrixToPermalink()
}
}
override suspend fun getPermalinkFor(eventId: EventId): Result<String> = withContext(roomDispatcher) {
runCatching {
innerRoom.matrixToEventPermalink(eventId.value)
}
}
override suspend fun getRoomVisibility(): Result<RoomVisibility> = withContext(roomDispatcher) {
runCatching {
innerRoom.getRoomVisibility().map()
}
}
override suspend fun getUpdatedIsEncrypted(): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.latestEncryptionState() == EncryptionState.ENCRYPTED
}
}
override suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result<Unit> = withContext(roomDispatcher) {
runCatching {
Timber.d("saveComposerDraft: $composerDraft into $roomId")
innerRoom.saveComposerDraft(composerDraft.into())
}
}
override suspend fun loadComposerDraft(): Result<ComposerDraft?> = withContext(roomDispatcher) {
runCatching {
Timber.d("loadComposerDraft for $roomId")
innerRoom.loadComposerDraft()?.into()
}
}
override suspend fun clearComposerDraft(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
Timber.d("clearComposerDraft for $roomId")
innerRoom.clearComposerDraft()
}
}
}

View file

@ -7,20 +7,20 @@
package io.element.android.libraries.matrix.impl.room
import androidx.collection.lruCache
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
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomPreview
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.roomlist.fullRoomWithTimeline
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
@ -28,17 +28,18 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.Membership
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
private const val CACHE_SIZE = 16
class RustRoomFactory(
private val sessionId: SessionId,
private val deviceId: DeviceId,
private val innerClient: Client,
private val notificationSettingsService: NotificationSettingsService,
private val sessionCoroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
@ -53,23 +54,14 @@ class RustRoomFactory(
) {
private val dispatcher = dispatchers.io.limitedParallelism(1)
private val mutex = Mutex()
private var isDestroyed: Boolean = false
private val isDestroyed: AtomicBoolean = AtomicBoolean(false)
private data class RustRoomReferences(
val roomListItem: RoomListItem,
val fullRoom: Room,
val room: SdkRoom,
)
private val cache = lruCache<RoomId, RustRoomReferences>(
maxSize = CACHE_SIZE,
onEntryRemoved = { evicted, roomId, oldRoom, _ ->
Timber.d("On room removed from cache: $roomId, evicted: $evicted")
oldRoom.roomListItem.close()
oldRoom.fullRoom.close()
}
)
private val matrixRoomInfoMapper = MatrixRoomInfoMapper()
private val roomInfoMapper = RoomInfoMapper()
private val eventFilters = TimelineConfig.excludedEvents
.takeIf { it.isNotEmpty() }
@ -81,102 +73,123 @@ class RustRoomFactory(
withContext(NonCancellable + dispatcher) {
mutex.withLock {
Timber.d("Destroying room factory")
cache.snapshot().values.forEach { (listItem, innerRoom) ->
innerRoom.destroy()
listItem.destroy()
}
cache.evictAll()
isDestroyed = true
isDestroyed.set(true)
}
}
}
suspend fun create(roomId: RoomId): MatrixRoom? = withContext(dispatcher) {
suspend fun getBaseRoom(roomId: RoomId): RustBaseRoom? = withContext(dispatcher) {
mutex.withLock {
if (isDestroyed) {
if (isDestroyed.get()) {
Timber.d("Room factory is destroyed, returning null for $roomId")
return@withContext null
}
var roomReferences: RustRoomReferences? = getRoomReferences(roomId)
if (roomReferences == null) {
// ... otherwise, lets wait for the SS to load all rooms and check again.
roomListService.allRooms.awaitLoaded()
roomReferences = getRoomReferences(roomId)
}
if (roomReferences == null) {
Timber.d("No room found for $roomId, returning null")
return@withContext null
}
val liveTimeline = roomReferences.fullRoom.timeline()
val initialRoomInfo = roomReferences.fullRoom.roomInfo()
RustMatrixRoom(
sessionId = sessionId,
deviceId = deviceId,
innerRoom = roomReferences.fullRoom,
innerTimeline = liveTimeline,
sessionCoroutineScope = sessionCoroutineScope,
notificationSettingsService = notificationSettingsService,
coroutineDispatchers = dispatchers,
systemClock = systemClock,
roomContentForwarder = roomContentForwarder,
roomSyncSubscriber = roomSyncSubscriber,
matrixRoomInfoMapper = matrixRoomInfoMapper,
featureFlagService = featureFlagService,
roomMembershipObserver = roomMembershipObserver,
initialRoomInfo = matrixRoomInfoMapper.map(initialRoomInfo),
)
val roomReferences = awaitRoomReferences(roomId) ?: return@withContext null
getBaseRoom(roomReferences)
}
}
suspend fun createRoomPreview(roomId: RoomId): RoomPreview? = withContext(dispatcher) {
if (isDestroyed) {
Timber.d("Room factory is destroyed, returning null for $roomId")
return@withContext null
}
val roomListItem = innerRoomListService.roomOrNull(roomId.value)
if (roomListItem == null) {
Timber.d("Room not found for $roomId")
return@withContext null
}
if (roomListItem.membership() !in RustRoomPreview.ALLOWED_MEMBERSHIPS) {
Timber.d("Room $roomId is not in allowed membership")
return@withContext null
}
val innerRoom = try {
roomListItem.previewRoom(via = emptyList())
} catch (e: Exception) {
Timber.e(e, "Failed to get room preview for $roomId")
return@withContext null
}
RustRoomPreview(
private suspend fun getBaseRoom(roomReferences: RustRoomReferences): RustBaseRoom? {
val initialRoomInfo = roomReferences.room.roomInfo()
return RustBaseRoom(
sessionId = sessionId,
inner = innerRoom,
deviceId = deviceId,
innerRoom = roomReferences.room,
coroutineDispatchers = dispatchers,
roomSyncSubscriber = roomSyncSubscriber,
roomMembershipObserver = roomMembershipObserver,
initialRoomInfo = roomInfoMapper.map(initialRoomInfo),
sessionCoroutineScope = sessionCoroutineScope,
)
}
private suspend fun getRoomReferences(roomId: RoomId): RustRoomReferences? {
cache[roomId]?.let {
Timber.d("Room found in cache for $roomId")
return it
suspend fun getJoinedRoomOrPreview(roomId: RoomId): GetRoomResult? = withContext(dispatcher) {
mutex.withLock {
if (isDestroyed.get()) {
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
// Init the live timeline in the SDK from the RoomListItem
if (!roomReferences.roomListItem.isTimelineInitialized()) {
roomReferences.roomListItem.initTimeline(eventFilters, "LIVE")
}
GetRoomResult.Joined(
JoinedRustRoom(
baseRoom = baseRoom,
notificationSettingsService = notificationSettingsService,
roomContentForwarder = roomContentForwarder,
liveInnerTimeline = roomReferences.room.timeline(),
coroutineDispatchers = dispatchers,
systemClock = systemClock,
roomInfoMapper = roomInfoMapper,
featureFlagService = featureFlagService,
)
)
} else {
val preview = try {
roomReferences.roomListItem.previewRoom(via = emptyList())
} catch (e: Exception) {
Timber.e(e, "Failed to get room preview for $roomId")
return@withContext null
}
GetRoomResult.NotJoined(
NotJoinedRustRoom(
sessionId = sessionId,
localRoom = getBaseRoom(roomReferences),
previewInfo = RoomPreviewInfoMapper.map(preview.info()),
)
)
}
}
}
private fun getRoomReferences(roomId: RoomId): RustRoomReferences? {
val roomListItem = innerRoomListService.roomOrNull(roomId.value)
if (roomListItem == null) {
Timber.d("Room not found for $roomId")
return null
}
val fullRoom = try {
roomListItem.fullRoomWithTimeline(filter = eventFilters)
} catch (e: RoomListException) {
Timber.e(e, "Failed to get full room with timeline for $roomId")
return null
}
Timber.d("Got full room with timeline for $roomId")
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,
fullRoom = fullRoom,
).also {
cache.put(roomId, it)
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
}
}
sealed interface GetRoomResult {
data class Joined(val joinedRoom: JoinedRoom) : GetRoomResult
data class NotJoined(val notJoinedRoom: NotJoinedRustRoom) : GetRoomResult
val room: BaseRoom?
get() = when (this) {
is Joined -> joinedRoom
is NotJoined -> notJoinedRoom.localRoom
}
}

View file

@ -1,59 +0,0 @@
/*
* Copyright 2024 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 androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomPreview
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomPreview as InnerRoomPreview
@Immutable
class RustRoomPreview(
override val sessionId: SessionId,
private val inner: InnerRoomPreview,
private val roomMembershipObserver: RoomMembershipObserver?,
) : RoomPreview {
companion object {
val ALLOWED_MEMBERSHIPS = setOf(Membership.INVITED, Membership.KNOCKED, Membership.BANNED)
}
override val info: RoomPreviewInfo = RoomPreviewInfoMapper.map(inner.info())
override suspend fun leave(): Result<Unit> = runCatching {
inner.leave()
}.onSuccess {
when (info.membership) {
CurrentUserMembership.INVITED -> roomMembershipObserver?.notifyUserDeclinedInvite(info.roomId)
CurrentUserMembership.KNOCKED -> roomMembershipObserver?.notifyUserCanceledKnock(info.roomId)
else -> Unit
}
}
override suspend fun forget(): Result<Unit> = runCatching {
inner.forget()
}
override suspend fun membershipDetails(): Result<RoomMembershipDetails?> = runCatching {
val details = inner.ownMembershipDetails() ?: return@runCatching null
RoomMembershipDetails(
currentUserMember = RoomMemberMapper.map(details.roomMember),
senderMember = details.senderInfo?.let { RoomMemberMapper.map(it) },
)
}
override fun close() {
inner.destroy()
}
}

View file

@ -7,8 +7,8 @@
package io.element.android.libraries.matrix.impl.room.member
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.roomMembers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -42,8 +42,8 @@ internal class RoomMemberListFetcher(
private val updatedRoomMemberMutex = Mutex()
private val roomId = room.id()
private val _membersFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
val membersFlow: StateFlow<MatrixRoomMembersState> = _membersFlow
private val _membersFlow = MutableStateFlow<RoomMembersState>(RoomMembersState.Unknown)
val membersFlow: StateFlow<RoomMembersState> = _membersFlow
/**
* Fetches the room members for the given room.
@ -75,16 +75,16 @@ internal class RoomMemberListFetcher(
}
}
private suspend fun MutableStateFlow<MatrixRoomMembersState>.fetchCachedRoomMembers(asPendingState: Boolean = true) {
private suspend fun MutableStateFlow<RoomMembersState>.fetchCachedRoomMembers(asPendingState: Boolean = true) {
Timber.i("Loading cached members for room $roomId")
try {
// Send current member list with pending state to notify the UI that we are loading new members
emit(pendingWithCurrentMembers())
val members = parseAndEmitMembers(room.membersNoSync())
val newState = if (asPendingState) {
MatrixRoomMembersState.Pending(prevRoomMembers = members)
RoomMembersState.Pending(prevRoomMembers = members)
} else {
MatrixRoomMembersState.Ready(members)
RoomMembersState.Ready(members)
}
emit(newState)
} catch (exception: CancellationException) {
@ -92,22 +92,22 @@ internal class RoomMemberListFetcher(
throw exception
} catch (exception: Exception) {
Timber.e(exception, "Failed to load cached members for room $roomId")
emit(MatrixRoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
emit(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
}
}
private suspend fun MutableStateFlow<MatrixRoomMembersState>.fetchRemoteRoomMembers() {
private suspend fun MutableStateFlow<RoomMembersState>.fetchRemoteRoomMembers() {
try {
// Send current member list with pending state to notify the UI that we are loading new members
emit(pendingWithCurrentMembers())
// Start loading new members
emit(MatrixRoomMembersState.Ready(parseAndEmitMembers(room.members())))
emit(RoomMembersState.Ready(parseAndEmitMembers(room.members())))
} catch (exception: CancellationException) {
Timber.d("Cancelled loading updated members for room $roomId")
throw exception
} catch (exception: Exception) {
Timber.e(exception, "Failed to load updated members for room $roomId")
emit(MatrixRoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
emit(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
}
}
@ -129,5 +129,5 @@ internal class RoomMemberListFetcher(
}
}
private fun pendingWithCurrentMembers() = MatrixRoomMembersState.Pending(_membersFlow.value.roomMembers().orEmpty().toImmutableList())
private fun pendingWithCurrentMembers() = RoomMembersState.Pending(_membersFlow.value.roomMembers().orEmpty().toImmutableList())
}

View file

@ -7,12 +7,12 @@
package io.element.android.libraries.matrix.impl.room.powerlevels
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import org.matrix.rustcomponents.sdk.RoomPowerLevels as RustRoomPowerLevels
object RoomPowerLevelsMapper {
fun map(roomPowerLevels: RustRoomPowerLevels): MatrixRoomPowerLevels {
return MatrixRoomPowerLevels(
fun map(roomPowerLevels: RustRoomPowerLevels): RoomPowerLevels {
return RoomPowerLevels(
ban = roomPowerLevels.ban,
invite = roomPowerLevels.invite,
kick = roomPowerLevels.kick,

View file

@ -8,14 +8,14 @@
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
import io.element.android.libraries.matrix.impl.room.RoomInfoMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
class RoomSummaryFactory(
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
private val roomInfoMapper: MatrixRoomInfoMapper = MatrixRoomInfoMapper(),
private val roomInfoMapper: RoomInfoMapper = RoomInfoMapper(),
) {
suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo().let(roomInfoMapper::map)

View file

@ -19,7 +19,7 @@ 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.IntentionalMention
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
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
@ -86,7 +86,7 @@ class RustTimeline(
private val inner: InnerTimeline,
mode: Timeline.Mode,
systemClock: SystemClock,
private val matrixRoom: MatrixRoom,
private val joinedRoom: JoinedRoom,
private val coroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher,
private val roomContentForwarder: RoomContentForwarder,
@ -137,7 +137,10 @@ class RustTimeline(
)
init {
coroutineScope.fetchMembers()
if (mode != Timeline.Mode.PINNED_EVENTS) {
coroutineScope.fetchMembers()
}
if (mode == Timeline.Mode.LIVE) {
// When timeline is live, we need to listen to the back pagination status as
// sdk can automatically paginate backwards.
@ -186,10 +189,10 @@ class RustTimeline(
}
}.onFailure { error ->
if (error is TimelineException.CannotPaginate) {
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}")
Timber.d("Can't paginate $direction on room ${joinedRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}")
} else {
updatePaginationStatus(direction) { it.copy(isPaginating = false) }
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
Timber.e(error, "Error paginating $direction on room ${joinedRoom.roomId}")
}
}.onSuccess { hasReachedEnd ->
updatePaginationStatus(direction) { it.copy(isPaginating = false, hasMoreToLoad = !hasReachedEnd) }
@ -209,7 +212,7 @@ class RustTimeline(
_timelineItems,
backwardPaginationStatus,
forwardPaginationStatus,
matrixRoom.roomInfoFlow.map { it.creator to it.isDm }.distinctUntilChanged(),
joinedRoom.roomInfoFlow.map { it.creator to it.isDm }.distinctUntilChanged(),
isTimelineInitialized,
) { timelineItems,
backwardPaginationStatus,
@ -261,7 +264,7 @@ class RustTimeline(
try {
inner.fetchMembers()
} catch (exception: Exception) {
Timber.e(exception, "Error fetching members for room ${matrixRoom.roomId}")
Timber.e(exception, "Error fetching members for room ${joinedRoom.roomId}")
}
}

View file

@ -9,12 +9,12 @@ package io.element.android.libraries.matrix.impl.analytics
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import kotlinx.coroutines.test.runTest
import org.junit.Test
class JoinedRoomExtKtTest {
class JoinedExtKtTest {
@Test
fun `test room size mapping`() = runTest {
mapOf(
@ -26,7 +26,7 @@ class JoinedRoomExtKtTest {
listOf(1001L, 2000L) to JoinedRoom.RoomSize.MoreThanAThousand
).forEach { (joinedMemberCounts, expectedRoomSize) ->
joinedMemberCounts.forEach { joinedMemberCount ->
assertThat(aMatrixRoom(joinedMemberCount = joinedMemberCount).toAnalyticsJoinedRoom(null))
assertThat(aRoom(joinedMemberCount = joinedMemberCount).toAnalyticsJoinedRoom(null))
.isEqualTo(
JoinedRoom(
isDM = false,
@ -41,7 +41,7 @@ class JoinedRoomExtKtTest {
@Test
fun `test isDirect parameter mapping`() = runTest {
assertThat(aMatrixRoom(isDirect = true).toAnalyticsJoinedRoom(null))
assertThat(aRoom(isDirect = true).toAnalyticsJoinedRoom(null))
.isEqualTo(
JoinedRoom(
isDM = true,
@ -54,7 +54,7 @@ class JoinedRoomExtKtTest {
@Test
fun `test isSpace parameter mapping`() = runTest {
assertThat(aMatrixRoom(isSpace = true).toAnalyticsJoinedRoom(null))
assertThat(aRoom(isSpace = true).toAnalyticsJoinedRoom(null))
.isEqualTo(
JoinedRoom(
isDM = false,
@ -67,7 +67,7 @@ class JoinedRoomExtKtTest {
@Test
fun `test trigger parameter mapping`() = runTest {
assertThat(aMatrixRoom(isDirect = false, isSpace = false, joinedMemberCount = 1).toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
assertThat(aRoom(isDirect = false, isSpace = false, joinedMemberCount = 1).toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
.isEqualTo(
JoinedRoom(
isDM = false,
@ -78,12 +78,12 @@ class JoinedRoomExtKtTest {
)
}
private fun aMatrixRoom(
private fun aRoom(
isDirect: Boolean = false,
isSpace: Boolean = false,
joinedMemberCount: Long = 0
): FakeMatrixRoom {
return FakeMatrixRoom().apply {
): FakeBaseRoom {
return FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo(isDirect = isDirect, isSpace = isSpace, joinedMembersCount = joinedMemberCount))
}
}

View file

@ -12,7 +12,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
@ -38,11 +38,11 @@ import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule
import org.matrix.rustcomponents.sdk.RoomHistoryVisibility as RustRoomHistoryVisibility
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
class MatrixRoomInfoMapperTest {
class RoomInfoMapperTest {
@Test
fun `mapping of RustRoomInfo should map all the fields`() {
assertThat(
MatrixRoomInfoMapper().map(
RoomInfoMapper().map(
aRustRoomInfo(
id = A_ROOM_ID.value,
displayName = "displayName",
@ -80,7 +80,7 @@ class MatrixRoomInfoMapperTest {
)
)
).isEqualTo(
MatrixRoomInfo(
RoomInfo(
id = A_ROOM_ID,
name = "displayName",
rawName = "rawName",
@ -127,7 +127,7 @@ class MatrixRoomInfoMapperTest {
@Test
fun `mapping of RustRoomInfo with null members should map all the fields`() {
assertThat(
MatrixRoomInfoMapper().map(
RoomInfoMapper().map(
aRustRoomInfo(
id = A_ROOM_ID.value,
displayName = null,
@ -164,7 +164,7 @@ class MatrixRoomInfoMapperTest {
)
)
).isEqualTo(
MatrixRoomInfo(
RoomInfo(
id = A_ROOM_ID,
name = null,
rawName = null,

View file

@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.services.analytics.test.FakeAnalyticsService
@ -33,7 +33,7 @@ class DefaultJoinRoomTest {
val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom().apply {
val roomResult = FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo())
}
val aTrigger = JoinedRoom.Trigger.MobilePermalink
@ -70,7 +70,7 @@ class DefaultJoinRoomTest {
val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom().apply {
val roomResult = FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo())
}
val aTrigger = JoinedRoom.Trigger.MobilePermalink
@ -108,7 +108,7 @@ class DefaultJoinRoomTest {
val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom().apply {
val roomResult = FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo())
}
val aTrigger = JoinedRoom.Trigger.MobilePermalink

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.room.member
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomMember
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
@ -40,16 +40,16 @@ class RoomMemberListFetcherTest {
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
fetcher.membersFlow.test {
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java)
fetcher.fetchRoomMembers(source = CACHE)
// Loading state
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java)
val cachedItemsState = awaitItem()
assertThat(cachedItemsState).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat((cachedItemsState as? MatrixRoomMembersState.Ready)?.roomMembers).hasSize(3)
assertThat(cachedItemsState).isInstanceOf(RoomMembersState.Ready::class.java)
assertThat((cachedItemsState as? RoomMembersState.Ready)?.roomMembers).hasSize(3)
}
}
@ -62,9 +62,9 @@ class RoomMemberListFetcherTest {
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
fetcher.membersFlow.test {
fetcher.fetchRoomMembers(source = CACHE)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers).isEmpty()
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java)
assertThat((awaitItem() as? RoomMembersState.Ready)?.roomMembers).isEmpty()
}
}
@ -77,9 +77,9 @@ class RoomMemberListFetcherTest {
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
fetcher.membersFlow.test {
fetcher.fetchRoomMembers(source = CACHE)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Error::class.java)
}
}
@ -100,11 +100,11 @@ class RoomMemberListFetcherTest {
fetcher.fetchRoomMembers(source = CACHE)
// Initial state
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java)
// Started loading cached members
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java)
// Finished loading cached members
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers).hasSize(3)
assertThat((awaitItem() as? RoomMembersState.Ready)?.roomMembers).hasSize(3)
ensureAllEventsConsumed()
}
@ -126,9 +126,9 @@ class RoomMemberListFetcherTest {
fetcher.membersFlow.test {
fetcher.fetchRoomMembers(source = SERVER)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java)
assertThat((awaitItem() as? RoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
}
}
@ -140,9 +140,9 @@ class RoomMemberListFetcherTest {
fetcher.membersFlow.test {
fetcher.fetchRoomMembers(source = SERVER)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Error::class.java)
}
}
@ -167,20 +167,20 @@ class RoomMemberListFetcherTest {
fetcher.membersFlow.test {
fetcher.fetchRoomMembers(source = CACHE_AND_SERVER)
// Initial
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java)
// Loading cached
awaitItem().let { pending ->
assertThat(pending).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat(pending).isInstanceOf(RoomMembersState.Pending::class.java)
assertThat(pending.roomMembers()).isEmpty()
}
// Loaded cached
awaitItem().let { cached ->
assertThat(cached).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat(cached).isInstanceOf(RoomMembersState.Pending::class.java)
assertThat(cached.roomMembers()).hasSize(1)
}
// Start loading new
awaitItem().let { ready ->
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat(ready).isInstanceOf(RoomMembersState.Ready::class.java)
assertThat(ready.roomMembers()).hasSize(3)
}
}

View file

@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.impl.room.powerlevels
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPowerLevels
import org.junit.Test
@ -31,7 +31,7 @@ class RoomPowerLevelsMapperTest {
)
)
).isEqualTo(
MatrixRoomPowerLevels(
RoomPowerLevels(
ban = 1,
invite = 2,
kick = 3,

View file

@ -24,7 +24,7 @@ import org.matrix.rustcomponents.sdk.RoomDirectorySearch
import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate
@OptIn(ExperimentalCoroutinesApi::class)
class RustRoomDirectoryListTest {
class RustBaseRoomDirectoryListTest {
@Test
fun `check that the state emits the expected values`() = runTest {
val roomDirectorySearch = FakeRustRoomDirectorySearch()

View file

@ -12,7 +12,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
class RustRoomDirectoryServiceTest {
class RustBaseRoomDirectoryServiceTest {
@Test
fun test() = runTest {
val client = FakeRustClient()

View file

@ -23,7 +23,7 @@ import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import org.matrix.rustcomponents.sdk.RoomListService as RustRoomListService
@OptIn(ExperimentalCoroutinesApi::class)
class RustRoomListServiceTest {
class RustBaseRoomListServiceTest {
@Test
fun `syncIndicator should emit the expected values`() = runTest {
val roomListService = FakeRustRoomListService()

View file

@ -12,7 +12,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListS
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimeline
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
@ -103,7 +103,7 @@ private fun TestScope.createRustTimeline(
inner: InnerTimeline,
mode: Timeline.Mode = Timeline.Mode.LIVE,
systemClock: SystemClock = FakeSystemClock(),
matrixRoom: MatrixRoom = FakeMatrixRoom().apply { givenRoomInfo(aRoomInfo()) },
joinedRoom: JoinedRoom = FakeJoinedRoom().apply { givenRoomInfo(aRoomInfo()) },
coroutineScope: CoroutineScope = backgroundScope,
dispatcher: CoroutineDispatcher = testCoroutineDispatchers().io,
roomContentForwarder: RoomContentForwarder = RoomContentForwarder(FakeRustRoomListService()),
@ -114,7 +114,7 @@ private fun TestScope.createRustTimeline(
inner = inner,
mode = mode,
systemClock = systemClock,
matrixRoom = matrixRoom,
joinedRoom = joinedRoom,
coroutineScope = coroutineScope,
dispatcher = dispatcher,
roomContentForwarder = roomContentForwarder,

View file

@ -22,9 +22,10 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.NotJoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomPreview
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
@ -77,7 +78,7 @@ class FakeMatrixClient(
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
)
},
private val getRoomPreviewResult: (RoomIdOrAlias, List<String>) -> Result<RoomPreview> = { _, _ -> Result.failure(AN_EXCEPTION) },
private val getNotJoinedRoomResult: (RoomIdOrAlias, List<String>) -> Result<NotJoinedRoom> = { _, _ -> Result.failure(AN_EXCEPTION) },
private val clearCacheLambda: () -> Unit = { lambdaError() },
private val userIdServerNameLambda: () -> String = { lambdaError() },
private val getUrlLambda: (String) -> Result<String> = { lambdaError() },
@ -102,7 +103,7 @@ class FakeMatrixClient(
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var findDmResult: RoomId? = A_ROOM_ID
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
private val getRoomResults = mutableMapOf<RoomId, BaseRoom>()
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
private var uploadMediaResult: Result<String> = Result.success(AN_AVATAR_URL)
@ -123,12 +124,12 @@ class FakeMatrixClient(
}
var logoutLambda: (Boolean, Boolean) -> Unit = { _, _ -> }
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
override suspend fun getRoom(roomId: RoomId): BaseRoom? {
return getRoomResults[roomId]
}
override suspend fun getPendingRoom(roomId: RoomId): RoomPreview? = simulateLongTask {
getRoomPreviewResult(RoomIdOrAlias.Id(roomId), emptyList()).getOrNull()
override suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? {
return getRoomResults[roomId] as? JoinedRoom
}
override suspend fun findDM(userId: UserId): RoomId? {
@ -250,7 +251,7 @@ class FakeMatrixClient(
findDmResult = result
}
fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom?) {
fun givenGetRoomResult(roomId: RoomId, result: BaseRoom?) {
if (result == null) {
getRoomResults.remove(roomId)
} else {
@ -294,8 +295,8 @@ class FakeMatrixClient(
resolveRoomAliasResult(roomAlias)
}
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomPreview> = simulateLongTask {
getRoomPreviewResult(roomIdOrAlias, serverNames)
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<NotJoinedRoom> = simulateLongTask {
getNotJoinedRoomResult(roomIdOrAlias, serverNames)
}
override suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> {

View file

@ -0,0 +1,220 @@
/*
* Copyright 2023, 2024 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.test.room
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.TestScope
class FakeBaseRoom(
override val sessionId: SessionId = A_SESSION_ID,
override val roomId: RoomId = A_ROOM_ID,
initialRoomInfo: RoomInfo = aRoomInfo(),
override val roomCoroutineScope: CoroutineScope = TestScope(),
private var roomPermalinkResult: () -> Result<String> = { lambdaError() },
private var eventPermalinkResult: (EventId) -> Result<String> = { lambdaError() },
private val userDisplayNameResult: (UserId) -> Result<String?> = { lambdaError() },
private val userAvatarUrlResult: () -> Result<String?> = { lambdaError() },
private val userRoleResult: () -> Result<RoomMember.Role> = { lambdaError() },
private val getUpdatedMemberResult: (UserId) -> Result<RoomMember> = { lambdaError() },
private val joinRoomResult: () -> Result<Unit> = { lambdaError() },
private val canInviteResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canKickResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canBanResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canRedactOwnResult: (UserId) -> Result<Boolean> = { lambdaError() },
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 canUserTriggerRoomNotificationResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canUserJoinCallResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canUserPinUnpinResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val setIsFavoriteResult: (Boolean) -> Result<Unit> = { lambdaError() },
private val powerLevelsResult: () -> Result<RoomPowerLevels> = { lambdaError() },
private val leaveRoomLambda: () -> Result<Unit> = { lambdaError() },
private val updateMembersResult: () -> Unit = { lambdaError() },
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
private val clearComposerDraftLambda: () -> Result<Unit> = { Result.success(Unit) },
private val subscribeToSyncLambda: () -> Unit = { lambdaError() },
private val getRoomVisibilityResult: () -> Result<RoomVisibility> = { lambdaError() },
private val forgetResult: () -> Result<Unit> = { lambdaError() },
) : BaseRoom {
private val _roomInfoFlow: MutableStateFlow<RoomInfo> = MutableStateFlow(initialRoomInfo)
override val roomInfoFlow: StateFlow<RoomInfo> = _roomInfoFlow
fun givenRoomInfo(roomInfo: RoomInfo) {
_roomInfoFlow.tryEmit(roomInfo)
}
override val membersStateFlow: MutableStateFlow<RoomMembersState> = MutableStateFlow(RoomMembersState.Unknown)
override suspend fun updateMembers() = updateMembersResult()
override suspend fun getUpdatedMember(userId: UserId): Result<RoomMember> {
return getUpdatedMemberResult(userId)
}
override suspend fun getMembers(limit: Int): Result<List<RoomMember>> {
return getMembersResult(limit)
}
override suspend fun subscribeToSync() {
subscribeToSyncLambda()
}
override suspend fun powerLevels(): Result<RoomPowerLevels> {
return powerLevelsResult()
}
override fun destroy() = Unit
override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask {
userDisplayNameResult(userId)
}
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = simulateLongTask {
userAvatarUrlResult()
}
override suspend fun userRole(userId: UserId): Result<RoomMember.Role> {
return userRoleResult()
}
override suspend fun getPermalink(): Result<String> {
return roomPermalinkResult()
}
override suspend fun getPermalinkFor(eventId: EventId): Result<String> {
return eventPermalinkResult(eventId)
}
override suspend fun getRoomVisibility(): Result<RoomVisibility> = simulateLongTask {
getRoomVisibilityResult()
}
override suspend fun leave(): Result<Unit> = simulateLongTask {
return leaveRoomLambda()
}
override suspend fun join(): Result<Unit> {
return joinRoomResult()
}
override suspend fun forget(): Result<Unit> {
return forgetResult()
}
override suspend fun canUserBan(userId: UserId): Result<Boolean> {
return canBanResult(userId)
}
override suspend fun canUserKick(userId: UserId): Result<Boolean> {
return canKickResult(userId)
}
override suspend fun canUserInvite(userId: UserId): Result<Boolean> {
return canInviteResult(userId)
}
override suspend fun canUserRedactOwn(userId: UserId): Result<Boolean> {
return canRedactOwnResult(userId)
}
override suspend fun canUserRedactOther(userId: UserId): Result<Boolean> {
return canRedactOtherResult(userId)
}
override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean> {
return canSendStateResult(userId, type)
}
override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result<Boolean> {
return canUserSendMessageResult(userId, type)
}
override suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean> {
return canUserTriggerRoomNotificationResult(userId)
}
override suspend fun canUserJoinCall(userId: UserId): Result<Boolean> {
return canUserJoinCallResult(userId)
}
override suspend fun canUserPinUnpin(userId: UserId): Result<Boolean> {
return canUserPinUnpinResult(userId)
}
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> {
return setIsFavoriteResult(isFavorite)
}
val markAsReadCalls = mutableListOf<ReceiptType>()
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> {
markAsReadCalls.add(receiptType)
return Result.success(Unit)
}
var setUnreadFlagCalls = mutableListOf<Boolean>()
private set
override suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit> {
setUnreadFlagCalls.add(isUnread)
return Result.success(Unit)
}
override suspend fun saveComposerDraft(composerDraft: ComposerDraft) = saveComposerDraftLambda(composerDraft)
override suspend fun loadComposerDraft() = loadComposerDraftLambda()
override suspend fun clearComposerDraft() = clearComposerDraftLambda()
override suspend fun getUpdatedIsEncrypted(): Result<Boolean> = simulateLongTask {
Result.success(info().isEncrypted.orFalse())
}
fun givenRoomMembersState(state: RoomMembersState) {
membersStateFlow.value = state
}
override suspend fun clearEventCacheStorage(): Result<Unit> {
return Result.success(Unit)
}
}
fun defaultRoomPowerLevels() = RoomPowerLevels(
ban = 50,
invite = 0,
kick = 50,
sendEvents = 0,
redactEvents = 50,
roomName = 100,
roomAvatar = 100,
roomTopic = 100
)

View file

@ -1,5 +1,5 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* 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.
@ -14,7 +14,6 @@ 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.SessionId
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
@ -23,33 +22,26 @@ 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
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState
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.MatrixRoomPowerLevels
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.ReceiptType
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.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
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
@ -58,36 +50,25 @@ import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
import java.io.File
class FakeMatrixRoom(
override val sessionId: SessionId = A_SESSION_ID,
override val roomId: RoomId = A_ROOM_ID,
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
class FakeJoinedRoom(
val baseRoom: FakeBaseRoom = FakeBaseRoom(),
override val liveTimeline: Timeline = FakeTimeline(),
initialRoomInfo: MatrixRoomInfo = aRoomInfo(),
override val roomCoroutineScope: CoroutineScope = TestScope(),
private var roomPermalinkResult: () -> Result<String> = { lambdaError() },
private var eventPermalinkResult: (EventId) -> Result<String> = { lambdaError() },
private val sendCallNotificationIfNeededResult: () -> Result<Unit> = { lambdaError() },
private val userDisplayNameResult: (UserId) -> Result<String?> = { lambdaError() },
private val userAvatarUrlResult: () -> Result<String?> = { lambdaError() },
private val userRoleResult: () -> Result<RoomMember.Role> = { lambdaError() },
private val getUpdatedMemberResult: (UserId) -> Result<RoomMember> = { lambdaError() },
private val joinRoomResult: () -> Result<Unit> = { lambdaError() },
private val inviteUserResult: (UserId) -> Result<Unit> = { lambdaError() },
private val canInviteResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canKickResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canBanResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canRedactOwnResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canRedactOtherResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canSendStateResult: (UserId, StateEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
private val canUserSendMessageResult: (UserId, MessageEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
override val syncUpdateFlow: StateFlow<Long> = MutableStateFlow(0),
override val roomTypingMembersFlow: Flow<List<UserId>> = MutableStateFlow(emptyList()),
override val identityStateChangesFlow: Flow<List<IdentityStateChange>> = MutableStateFlow(emptyList()),
override val roomNotificationSettingsStateFlow: StateFlow<RoomNotificationSettingsState> =
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> =
@ -98,233 +79,65 @@ class FakeMatrixRoom(
{ _, _, _, _, _, _ -> lambdaError() },
private val sendVoiceMessageResult: (File, AudioInfo, List<Float>, ProgressCallback?, ReplyParameters?) -> 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() },
private val removeAvatarResult: () -> Result<Unit> = { lambdaError() },
private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
private val sendMessageResult: (String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _ -> lambdaError() },
private val updateUserRoleResult: () -> Result<Unit> = { lambdaError() },
private val toggleReactionResult: (String, EventOrTransactionId) -> Result<Unit> = { _, _ -> lambdaError() },
private val cancelSendResult: (TransactionId) -> Result<Unit> = { lambdaError() },
private val forwardEventResult: (EventId, List<RoomId>) -> Result<Unit> = { _, _ -> lambdaError() },
private val reportContentResult: (EventId, String, UserId?) -> Result<Unit> = { _, _, _ -> lambdaError() },
private val kickUserResult: (UserId, String?) -> Result<Unit> = { _, _ -> lambdaError() },
private val banUserResult: (UserId, String?) -> Result<Unit> = { _, _ -> lambdaError() },
private val unBanUserResult: (UserId, String?) -> Result<Unit> = { _, _ -> 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 progressCallbackValues: List<Pair<Long, Long>> = emptyList(),
private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result<String> = { _, _, _, _ -> lambdaError() },
private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result<MatrixWidgetDriver> = { lambdaError() },
private val canUserTriggerRoomNotificationResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canUserJoinCallResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canUserPinUnpinResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val setIsFavoriteResult: (Boolean) -> Result<Unit> = { lambdaError() },
private val powerLevelsResult: () -> Result<MatrixRoomPowerLevels> = { lambdaError() },
private val updatePowerLevelsResult: () -> Result<Unit> = { lambdaError() },
private val resetPowerLevelsResult: () -> Result<MatrixRoomPowerLevels> = { lambdaError() },
private val typingNoticeResult: (Boolean) -> Result<Unit> = { lambdaError() },
private val leaveRoomLambda: () -> Result<Unit> = { lambdaError() },
private val updateMembersResult: () -> Unit = { lambdaError() },
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
private val createTimelineResult: (CreateTimelineParams) -> Result<Timeline> = { lambdaError() },
private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> },
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
private val clearComposerDraftLambda: () -> Result<Unit> = { Result.success(Unit) },
private val subscribeToSyncLambda: () -> 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() },
private val updateAvatarResult: (String, ByteArray) -> Result<Unit> = { _, _ -> lambdaError() },
private val removeAvatarResult: () -> Result<Unit> = { lambdaError() },
private val updateUserRoleResult: (List<UserRoleChange>) -> Result<Unit> = { lambdaError() },
private val updatePowerLevelsResult: (RoomPowerLevels) -> Result<Unit> = { lambdaError() },
private val resetPowerLevelsResult: () -> Result<RoomPowerLevels> = { lambdaError() },
private val reportContentResult: (EventId, String, UserId?) -> Result<Unit> = { _, _, _ -> lambdaError() },
private val kickUserResult: (UserId, String?) -> Result<Unit> = { _, _ -> lambdaError() },
private val banUserResult: (UserId, String?) -> Result<Unit> = { _, _ -> lambdaError() },
private val unBanUserResult: (UserId, String?) -> Result<Unit> = { _, _ -> lambdaError() },
private val ignoreDeviceTrustAndResendResult: (Map<UserId, List<DeviceId>>, SendHandle) -> Result<Unit> = { _, _ -> lambdaError() },
private val withdrawVerificationAndResendResult: (List<UserId>, SendHandle) -> Result<Unit> = { _, _ -> lambdaError() },
private val updateCanonicalAliasResult: (RoomAlias?, List<RoomAlias>) -> Result<Unit> = { _, _ -> lambdaError() },
private val updateRoomVisibilityResult: (RoomVisibility) -> Result<Unit> = { lambdaError() },
private val updateRoomHistoryVisibilityResult: (RoomHistoryVisibility) -> Result<Unit> = { lambdaError() },
private val roomVisibilityResult: () -> Result<RoomVisibility> = { lambdaError() },
private val publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result<Boolean> = { lambdaError() },
private val removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result<Boolean> = { lambdaError() },
private val enableEncryptionResult: () -> Result<Unit> = { lambdaError() },
private val updateJoinRuleResult: (JoinRule) -> Result<Unit> = { lambdaError() },
) : MatrixRoom {
private val _roomInfoFlow: MutableStateFlow<MatrixRoomInfo> = MutableStateFlow(initialRoomInfo)
override val roomInfoFlow: StateFlow<MatrixRoomInfo> = _roomInfoFlow
fun givenRoomInfo(roomInfo: MatrixRoomInfo) {
_roomInfoFlow.tryEmit(roomInfo)
private val setSendQueueEnabledResult: (Boolean) -> Unit = { _: Boolean -> },
) : JoinedRoom, BaseRoom by baseRoom {
fun givenRoomMembersState(state: RoomMembersState) {
baseRoom.givenRoomMembersState(state)
}
private val _roomTypingMembersFlow: MutableSharedFlow<List<UserId>> = MutableSharedFlow(replay = 1)
override val roomTypingMembersFlow: Flow<List<UserId>> = _roomTypingMembersFlow
fun givenRoomTypingMembers(typingMembers: List<UserId>) {
_roomTypingMembersFlow.tryEmit(typingMembers)
fun givenRoomInfo(roomInfo: RoomInfo) {
baseRoom.givenRoomInfo(roomInfo)
}
private val _identityStateChangesFlow: MutableSharedFlow<List<IdentityStateChange>> = MutableSharedFlow(replay = 1)
override val identityStateChangesFlow: Flow<List<IdentityStateChange>> = _identityStateChangesFlow
fun emitIdentityStateChanges(identityStateChanges: List<IdentityStateChange>) {
_identityStateChangesFlow.tryEmit(identityStateChanges)
}
private val _knockRequestsFlow: MutableSharedFlow<List<KnockRequest>> = MutableSharedFlow(replay = 1)
override val knockRequestsFlow: Flow<List<KnockRequest>> = _knockRequestsFlow
fun emitKnockRequests(knockRequests: List<KnockRequest>) {
_knockRequestsFlow.tryEmit(knockRequests)
}
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> =
MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown)
override suspend fun updateMembers() = updateMembersResult()
override suspend fun getUpdatedMember(userId: UserId): Result<RoomMember> {
return getUpdatedMemberResult(userId)
}
override suspend fun getMembers(limit: Int): Result<List<RoomMember>> {
return getMembersResult(limit)
}
override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask {
val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, info().isEncrypted.orFalse(), isOneToOne).getOrThrow()
roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings)
return Result.success(Unit)
}
override suspend fun enableEncryption(): Result<Unit> = simulateLongTask {
enableEncryptionResult().onSuccess {
givenRoomInfo(info().copy(isEncrypted = true))
emitSyncUpdate()
}
}
private val _syncUpdateFlow = MutableStateFlow(0L)
override val syncUpdateFlow: StateFlow<Long> = _syncUpdateFlow.asStateFlow()
fun emitSyncUpdate() {
_syncUpdateFlow.tryEmit(_syncUpdateFlow.value + 1)
}
override suspend fun createTimeline(
createTimelineParams: CreateTimelineParams,
): Result<Timeline> = simulateLongTask {
override suspend fun createTimeline(createTimelineParams: CreateTimelineParams): Result<Timeline> = simulateLongTask {
createTimelineResult(createTimelineParams)
}
override suspend fun subscribeToSync() {
subscribeToSyncLambda()
}
override suspend fun powerLevels(): Result<MatrixRoomPowerLevels> {
return powerLevelsResult()
}
override suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result<Unit> = simulateLongTask {
updatePowerLevelsResult()
}
override suspend fun resetPowerLevels(): Result<MatrixRoomPowerLevels> = simulateLongTask {
resetPowerLevelsResult()
}
override fun destroy() = Unit
override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask {
userDisplayNameResult(userId)
}
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = simulateLongTask {
userAvatarUrlResult()
}
override suspend fun userRole(userId: UserId): Result<RoomMember.Role> {
return userRoleResult()
}
override suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit> {
return updateUserRoleResult()
}
override suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>) = simulateLongTask {
editMessageLambda(eventId, body, htmlBody, intentionalMentions)
}
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>) = simulateLongTask {
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit> = simulateLongTask {
sendMessageResult(body, htmlBody, intentionalMentions)
}
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> {
return toggleReactionResult(emoji, eventOrTransactionId)
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> {
return cancelSendResult(transactionId)
}
override suspend fun getPermalink(): Result<String> {
return roomPermalinkResult()
}
override suspend fun getPermalinkFor(eventId: EventId): Result<String> {
return eventPermalinkResult(eventId)
}
override suspend fun leave(): Result<Unit> {
return leaveRoomLambda()
}
override suspend fun join(): Result<Unit> {
return joinRoomResult()
}
override suspend fun inviteUserById(id: UserId): Result<Unit> = simulateLongTask {
inviteUserResult(id)
}
override suspend fun canUserBan(userId: UserId): Result<Boolean> {
return canBanResult(userId)
}
override suspend fun canUserKick(userId: UserId): Result<Boolean> {
return canKickResult(userId)
}
override suspend fun canUserInvite(userId: UserId): Result<Boolean> {
return canInviteResult(userId)
}
override suspend fun canUserRedactOwn(userId: UserId): Result<Boolean> {
return canRedactOwnResult(userId)
}
override suspend fun canUserRedactOther(userId: UserId): Result<Boolean> {
return canRedactOtherResult(userId)
}
override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean> {
return canSendStateResult(userId, type)
}
override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result<Boolean> {
return canUserSendMessageResult(userId, type)
}
override suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean> {
return canUserTriggerRoomNotificationResult(userId)
}
override suspend fun canUserJoinCall(userId: UserId): Result<Boolean> {
return canUserJoinCallResult(userId)
}
override suspend fun canUserPinUnpin(userId: UserId): Result<Boolean> {
return canUserPinUnpinResult(userId)
override suspend fun editMessage(
eventId: EventId,
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>
): Result<Unit> = simulateLongTask {
editMessageLambda(eventId, body, htmlBody, intentionalMentions)
}
override suspend fun sendImage(
@ -440,78 +253,7 @@ class FakeMatrixRoom(
)
}
private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
progressCallbackValues.forEach { (current, total) ->
progressCallback?.onProgress(current, total)
delay(1)
}
}
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = simulateLongTask {
forwardEventResult(eventId, roomIds)
}
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> = simulateLongTask {
updateAvatarResult(mimeType, data)
}
override suspend fun removeAvatar(): Result<Unit> = simulateLongTask {
removeAvatarResult()
}
override suspend fun setName(name: String): Result<Unit> = simulateLongTask {
setNameResult(name)
}
override suspend fun setTopic(topic: String): Result<Unit> = simulateLongTask {
setTopicResult(topic)
}
override suspend fun reportContent(
eventId: EventId,
reason: String,
blockUserId: UserId?
): Result<Unit> = simulateLongTask {
return reportContentResult(eventId, reason, blockUserId)
}
override suspend fun kickUser(userId: UserId, reason: String?): Result<Unit> {
return kickUserResult(userId, reason)
}
override suspend fun banUser(userId: UserId, reason: String?): Result<Unit> {
return banUserResult(userId, reason)
}
override suspend fun unbanUser(userId: UserId, reason: String?): Result<Unit> {
return unBanUserResult(userId, reason)
}
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> {
return setIsFavoriteResult(isFavorite)
}
val markAsReadCalls = mutableListOf<ReceiptType>()
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> {
markAsReadCalls.add(receiptType)
return Result.success(Unit)
}
var setUnreadFlagCalls = mutableListOf<Boolean>()
private set
override suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit> {
setUnreadFlagCalls.add(isUnread)
return Result.success(Unit)
}
override suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind
): Result<Unit> = simulateLongTask {
override suspend fun createPoll(question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind): Result<Unit> = simulateLongTask {
return createPollResult(
question,
answers,
@ -536,58 +278,53 @@ class FakeMatrixRoom(
)
}
override suspend fun sendPollResponse(
pollStartId: EventId,
answers: List<String>
): Result<Unit> = simulateLongTask {
return sendPollResponseResult(pollStartId, answers)
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 {
return endPollResult(pollStartId, text)
override suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit> = simulateLongTask {
endPollResult(
pollStartId,
text,
)
}
override suspend fun typingNotice(isTyping: Boolean): Result<Unit> {
return typingNoticeResult(isTyping)
override suspend fun typingNotice(isTyping: Boolean): Result<Unit> = simulateLongTask {
typingNoticeResult(isTyping)
}
override suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings,
clientId: String,
languageTag: String?,
theme: String?,
): Result<String> = generateWidgetWebViewUrlResult(
widgetSettings,
clientId,
languageTag,
theme,
)
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> {
return sendCallNotificationIfNeededResult()
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = simulateLongTask {
toggleReactionResult(emoji, eventOrTransactionId)
}
override suspend fun setSendQueueEnabled(enabled: Boolean) = setSendQueueEnabledLambda(enabled)
override suspend fun saveComposerDraft(composerDraft: ComposerDraft) = saveComposerDraftLambda(composerDraft)
override suspend fun loadComposerDraft() = loadComposerDraftLambda()
override suspend fun clearComposerDraft() = clearComposerDraftLambda()
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> {
return getWidgetDriverResult(widgetSettings)
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = simulateLongTask {
forwardEventResult(eventId, roomIds)
}
override suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, sendHandle: SendHandle): Result<Unit> = simulateLongTask {
return ignoreDeviceTrustAndResendResult(devices, sendHandle)
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = simulateLongTask {
cancelSendResult(transactionId)
}
override suspend fun withdrawVerificationAndResend(userIds: List<UserId>, sendHandle: SendHandle): Result<Unit> = simulateLongTask {
return withdrawVerificationAndResendResult(userIds, sendHandle)
override suspend fun inviteUserById(id: UserId): Result<Unit> = simulateLongTask {
inviteUserResult(id)
}
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> = simulateLongTask {
simulateSendMediaProgress(null)
updateAvatarResult(mimeType, data)
}
override suspend fun removeAvatar(): Result<Unit> = simulateLongTask {
removeAvatarResult()
}
override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask {
val notificationSettings = roomNotificationSettingsService.getRoomNotificationSettings(roomId, info().isEncrypted.orFalse(), isOneToOne).getOrThrow()
(roomNotificationSettingsStateFlow as MutableStateFlow).value = RoomNotificationSettingsState.Ready(notificationSettings)
return Result.success(Unit)
}
override suspend fun updateCanonicalAlias(canonicalAlias: RoomAlias?, alternativeAliases: List<RoomAlias>): Result<Unit> = simulateLongTask {
@ -602,10 +339,6 @@ class FakeMatrixRoom(
updateRoomHistoryVisibilityResult(historyVisibility)
}
override suspend fun getRoomVisibility(): Result<RoomVisibility> = simulateLongTask {
roomVisibilityResult()
}
override suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result<Boolean> = simulateLongTask {
publishRoomAliasInRoomDirectoryResult(roomAlias)
}
@ -614,30 +347,90 @@ class FakeMatrixRoom(
removeRoomAliasFromRoomDirectoryResult(roomAlias)
}
override suspend fun enableEncryption(): Result<Unit> = simulateLongTask {
enableEncryptionResult().onSuccess {
baseRoom.givenRoomInfo(info().copy(isEncrypted = true))
emitSyncUpdate()
}
}
override suspend fun updateJoinRule(joinRule: JoinRule): Result<Unit> = simulateLongTask {
updateJoinRuleResult(joinRule)
}
override suspend fun getUpdatedIsEncrypted(): Result<Boolean> = simulateLongTask {
Result.success(info().isEncrypted.orFalse())
override suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit> = simulateLongTask {
updateUserRoleResult(changes)
}
fun givenRoomMembersState(state: MatrixRoomMembersState) {
membersStateFlow.value = state
override suspend fun updatePowerLevels(roomPowerLevels: RoomPowerLevels): Result<Unit> = simulateLongTask {
updatePowerLevelsResult(roomPowerLevels)
}
override suspend fun clearEventCacheStorage(): Result<Unit> {
return Result.success(Unit)
override suspend fun resetPowerLevels(): Result<RoomPowerLevels> = simulateLongTask {
resetPowerLevelsResult()
}
override suspend fun setName(name: String): Result<Unit> = simulateLongTask {
setNameResult(name)
}
override suspend fun setTopic(topic: String): Result<Unit> = simulateLongTask {
setTopicResult(topic)
}
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = simulateLongTask {
reportContentResult(eventId, reason, blockUserId)
}
override suspend fun kickUser(userId: UserId, reason: String?): Result<Unit> = simulateLongTask {
kickUserResult(userId, reason)
}
override suspend fun banUser(userId: UserId, reason: String?): Result<Unit> = simulateLongTask {
banUserResult(userId, reason)
}
override suspend fun unbanUser(userId: UserId, reason: String?): Result<Unit> = simulateLongTask {
unBanUserResult(userId, reason)
}
override suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings,
clientId: String,
languageTag: String?,
theme: String?
): Result<String> = simulateLongTask {
generateWidgetWebViewUrlResult(widgetSettings, clientId, languageTag, theme)
}
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> {
return getWidgetDriverResult(widgetSettings)
}
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> = simulateLongTask {
sendCallNotificationIfNeededResult()
}
override suspend fun setSendQueueEnabled(enabled: Boolean) = simulateLongTask {
setSendQueueEnabledResult(enabled)
}
override suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, sendHandle: SendHandle): Result<Unit> = simulateLongTask {
ignoreDeviceTrustAndResendResult(devices, sendHandle)
}
override suspend fun withdrawVerificationAndResend(userIds: List<UserId>, sendHandle: SendHandle): Result<Unit> = simulateLongTask {
withdrawVerificationAndResendResult(userIds, sendHandle)
}
private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
progressCallbackValues.forEach { (current, total) ->
progressCallback?.onProgress(current, total)
delay(1)
}
}
fun emitSyncUpdate() {
(syncUpdateFlow as MutableStateFlow).value = syncUpdateFlow.value + 1
}
}
fun defaultRoomPowerLevels() = MatrixRoomPowerLevels(
ban = 50,
invite = 0,
kick = 50,
sendEvents = 0,
redactEvents = 50,
roomName = 100,
roomAvatar = 100,
roomTopic = 100
)

View file

@ -8,30 +8,19 @@
package io.element.android.libraries.matrix.test.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.NotJoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.RoomPreview
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
@Immutable
class FakeRoomPreview(
override val sessionId: SessionId = A_SESSION_ID,
override val info: RoomPreviewInfo = aRoomPreviewInfo(),
private val declineInviteResult: () -> Result<Unit> = { lambdaError() },
private val forgetRoomResult: () -> Result<Unit> = { lambdaError() },
class FakeNotJoinedRoom(
override val localRoom: BaseRoom? = null,
override val previewInfo: RoomPreviewInfo = aRoomPreviewInfo(),
private val roomMembershipDetails: () -> Result<RoomMembershipDetails?> = { lambdaError() },
) : RoomPreview {
override suspend fun leave(): Result<Unit> = simulateLongTask {
declineInviteResult()
}
override suspend fun forget(): Result<Unit> = simulateLongTask {
forgetRoomResult()
}
) : NotJoinedRoom {
override suspend fun membershipDetails(): Result<RoomMembershipDetails?> = simulateLongTask {
roomMembershipDetails()
}

View file

@ -12,7 +12,7 @@ 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.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
@ -61,7 +61,7 @@ fun aRoomInfo(
numUnreadNotifications: Long = 0,
numUnreadMentions: Long = 0,
historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined,
) = MatrixRoomInfo(
) = RoomInfo(
id = id,
name = name,
rawName = rawName,

View file

@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.test.room
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.SessionId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.RoomType
@ -19,20 +18,15 @@ import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.tests.testutils.lambda.lambdaError
fun aRoomPreview(
sessionId: SessionId = A_SESSION_ID,
localRoom: FakeBaseRoom? = null,
info: RoomPreviewInfo = aRoomPreviewInfo(),
declineInviteResult: () -> Result<Unit> = { lambdaError() },
forgetRoomResult: () -> Result<Unit> = { lambdaError() },
roomMembershipDetails: () -> Result<RoomMembershipDetails?> = { lambdaError() },
) = FakeRoomPreview(
sessionId = sessionId,
info = info,
declineInviteResult = declineInviteResult,
forgetRoomResult = forgetRoomResult,
) = FakeNotJoinedRoom(
localRoom = localRoom,
previewInfo = info,
roomMembershipDetails = roomMembershipDetails,
)

View file

@ -12,7 +12,7 @@ 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.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
@ -33,7 +33,7 @@ import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toPersistentList
fun aRoomSummary(
info: MatrixRoomInfo = aRoomInfo(),
info: RoomInfo = aRoomInfo(),
lastMessage: RoomMessage? = aRoomMessage(),
) = RoomSummary(
info = info,
@ -76,7 +76,7 @@ fun aRoomSummary(
historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined,
lastMessage: RoomMessage? = aRoomMessage(),
) = RoomSummary(
info = MatrixRoomInfo(
info = RoomInfo(
id = roomId,
name = name,
rawName = rawName,

View file

@ -7,14 +7,14 @@
package io.element.android.libraries.matrix.test.timeline
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class LiveTimelineProvider(
private val room: MatrixRoom,
private val room: JoinedRoom,
) : TimelineProvider {
override fun activeTimelineFlow(): StateFlow<Timeline> = MutableStateFlow(room.liveTimeline)
}

View file

@ -9,9 +9,9 @@ package io.element.android.libraries.matrix.ui.model
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomInfo
fun MatrixRoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
fun RoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
id = id.value,
name = name,
url = avatarUrl,

View file

@ -14,19 +14,19 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.roomMembers
@Composable
fun MatrixRoom.getRoomMemberAsState(userId: UserId): State<RoomMember?> {
fun BaseRoom.getRoomMemberAsState(userId: UserId): State<RoomMember?> {
val roomMembersState by membersStateFlow.collectAsState()
return getRoomMemberAsState(roomMembersState = roomMembersState, userId = userId)
}
@Composable
fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserId): State<RoomMember?> {
fun getRoomMemberAsState(roomMembersState: RoomMembersState, userId: UserId): State<RoomMember?> {
val roomMembers = roomMembersState.roomMembers()
return remember(roomMembers) {
derivedStateOf {
@ -38,7 +38,7 @@ fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserI
}
@Composable
fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): State<RoomMember?> {
fun BaseRoom.getDirectRoomMember(roomMembersState: RoomMembersState): State<RoomMember?> {
val roomMembers = roomMembersState.roomMembers()
val roomInfo by roomInfoFlow.collectAsState()
return remember(roomMembersState, roomInfo.isDirect) {
@ -52,6 +52,6 @@ fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): St
}
@Composable
fun MatrixRoom.getCurrentRoomMember(roomMembersState: MatrixRoomMembersState): State<RoomMember?> {
fun BaseRoom.getCurrentRoomMember(roomMembersState: RoomMembersState): State<RoomMember?> {
return getRoomMemberAsState(roomMembersState = roomMembersState, userId = sessionId)
}

View file

@ -12,7 +12,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.isDm
@ -25,77 +25,77 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
@Composable
fun MatrixRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): State<Boolean> {
fun BaseRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): State<Boolean> {
return produceState(initialValue = true, key1 = updateKey) {
value = canSendMessage(type).getOrElse { true }
}
}
@Composable
fun MatrixRoom.canInviteAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canInviteAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canInvite().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canRedactOwnAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canRedactOwnAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canRedactOwn().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canRedactOtherAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canRedactOtherAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canRedactOther().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canCall(updateKey: Long): State<Boolean> {
fun BaseRoom.canCall(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canUserJoinCall(sessionId).getOrElse { false }
}
}
@Composable
fun MatrixRoom.canPinUnpin(updateKey: Long): State<Boolean> {
fun BaseRoom.canPinUnpin(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canUserPinUnpin(sessionId).getOrElse { false }
}
}
@Composable
fun MatrixRoom.isDmAsState(): State<Boolean> {
fun BaseRoom.isDmAsState(): State<Boolean> {
return produceState(initialValue = false) {
roomInfoFlow.collect { value = it.isDm }
}
}
@Composable
fun MatrixRoom.canKickAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canKickAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canKick().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canBanAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canBanAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canBan().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canHandleKnockRequestsAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canHandleKnockRequestsAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canHandleKnockRequests().getOrElse { false }
}
}
@Composable
fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
fun BaseRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
return produceState(initialValue = 0, key1 = updateKey) {
value = userRole(sessionId)
.getOrDefault(RoomMember.Role.USER)
@ -104,26 +104,26 @@ fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
}
@Composable
fun MatrixRoom.isOwnUserAdmin(): Boolean {
fun BaseRoom.isOwnUserAdmin(): Boolean {
val roomInfo by roomInfoFlow.collectAsState()
val powerLevel = roomInfo.userPowerLevels[sessionId] ?: 0L
return RoomMember.Role.forPowerLevel(powerLevel) == RoomMember.Role.ADMIN
}
@Composable
fun MatrixRoom.rawName(): String? {
fun BaseRoom.rawName(): String? {
val roomInfo by roomInfoFlow.collectAsState()
return roomInfo.rawName
}
@Composable
fun MatrixRoom.topic(): String? {
fun BaseRoom.topic(): String? {
val roomInfo by roomInfoFlow.collectAsState()
return roomInfo.topic
}
@Composable
fun MatrixRoom.avatarUrl(): String? {
fun BaseRoom.avatarUrl(): String? {
val roomInfo by roomInfoFlow.collectAsState()
return roomInfo.avatarUrl
}

View file

@ -12,7 +12,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.ui.model.getAvatarData
@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@OptIn(ExperimentalCoroutinesApi::class)
fun MatrixRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIdentityStateChange>> {
fun JoinedRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIdentityStateChange>> {
return roomInfoFlow
.filter {
// Room cannot become unencrypted, so we can just apply a filter here.
@ -52,7 +52,7 @@ fun MatrixRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIde
}
}
fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange(room: MatrixRoom) {
fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange(room: JoinedRoom) {
room.roomMemberIdentityStateChange()
.onEach { roomMemberIdentityStateChanges ->
value = roomMemberIdentityStateChanges.toPersistentList()

View file

@ -11,26 +11,26 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
class MatrixRoomMembersTest {
class RoomMembersTest {
private val roomMember1 = aRoomMember(A_USER_ID)
private val roomMember2 = aRoomMember(A_USER_ID_2)
private val roomMember3 = aRoomMember(A_USER_ID_3)
@Test
fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(
isDirect = true,
@ -38,8 +38,8 @@ class MatrixRoomMembersTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
)
}.test {
assertThat(awaitItem().value).isEqualTo(roomMember2)
@ -48,13 +48,13 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit null if the room is not a dm`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(isDirect = false)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
)
}.test {
assertThat(awaitItem().value).isNull()
@ -63,7 +63,7 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(
isDirect = true,
@ -71,8 +71,8 @@ class MatrixRoomMembersTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
)
}.test {
assertThat(awaitItem().value).isEqualTo(roomMember2)
@ -81,13 +81,13 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit null if the room has only 1 member`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(isDirect = true)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1))
)
}.test {
assertThat(awaitItem().value).isNull()
@ -96,14 +96,14 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit null if the room has only 3 members`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
).apply {
givenRoomInfo(aRoomInfo(isDirect = true))
}
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3))
)
}.test {
assertThat(awaitItem().value).isNull()
@ -112,13 +112,13 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit null if the other member is not active`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(isDirect = true),
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(
persistentListOf(
roomMember1,
roomMember2.copy(membership = RoomMembershipState.BAN),
@ -132,7 +132,7 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(
isDirect = true,
@ -140,8 +140,8 @@ class MatrixRoomMembersTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(
persistentListOf(
roomMember1,
roomMember2,
@ -156,10 +156,10 @@ class MatrixRoomMembersTest {
@Test
fun `getCurrentRoomMember returns the current user`() = runTest {
val matrixRoom = FakeMatrixRoom(sessionId = A_USER_ID)
val joinedRoom = FakeBaseRoom(sessionId = A_USER_ID)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getCurrentRoomMember(
MatrixRoomMembersState.Ready(
joinedRoom.getCurrentRoomMember(
RoomMembersState.Ready(
persistentListOf(
roomMember1,
roomMember2,
@ -174,10 +174,10 @@ class MatrixRoomMembersTest {
@Test
fun `getCurrentRoomMember returns null if the member is not found`() = runTest {
val matrixRoom = FakeMatrixRoom(sessionId = A_USER_ID)
val joinedRoom = FakeBaseRoom(sessionId = A_USER_ID)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getCurrentRoomMember(
MatrixRoomMembersState.Ready(
joinedRoom.getCurrentRoomMember(
RoomMembersState.Ready(
persistentListOf(
roomMember2,
roomMember3,

View file

@ -11,7 +11,7 @@ import android.net.Uri
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.JoinedRoom
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.coroutines.CancellationException
@ -22,7 +22,7 @@ import javax.inject.Inject
class MediaSender @Inject constructor(
private val preProcessor: MediaPreProcessor,
private val room: MatrixRoom,
private val room: JoinedRoom,
private val sessionPreferencesStore: SessionPreferencesStore,
) {
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
@ -130,7 +130,7 @@ class MediaSender @Inject constructor(
ongoingUploadJobs.remove(Job)
}
private suspend fun MatrixRoom.sendMedia(
private suspend fun JoinedRoom.sendMedia(
uploadInfo: MediaUploadInfo,
progressCallback: ProgressCallback?,
caption: String?,

View file

@ -13,10 +13,10 @@ import io.element.android.libraries.core.mimetype.MimeTypes
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.JoinedRoom
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
@ -45,12 +45,12 @@ class MediaSenderTest {
}
@Test
fun `given an attachment when sending it the MatrixRoom will call sendMedia`() = runTest {
fun `given an attachment when sending it the Room will call sendMedia`() = runTest {
val sendImageResult =
lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? ->
Result.success(FakeMediaUploadHandler())
}
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
sendImageResult = sendImageResult
)
val sender = createMediaSender(room = room)
@ -78,7 +78,7 @@ class MediaSenderTest {
lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? ->
Result.failure<FakeMediaUploadHandler>(Exception())
}
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
sendImageResult = sendImageResult
)
val sender = createMediaSender(room = room)
@ -96,7 +96,7 @@ class MediaSenderTest {
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
sendFileResult = sendFileResult
)
val sender = createMediaSender(room = room)
@ -123,7 +123,7 @@ class MediaSenderTest {
private fun createMediaSender(
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
room: MatrixRoom = FakeMatrixRoom(),
room: JoinedRoom = FakeJoinedRoom(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
) = MediaSender(
preProcessor = preProcessor,

View file

@ -10,7 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.datasource
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
import javax.inject.Inject
@ -24,7 +24,7 @@ interface FocusedTimelineMediaGalleryDataSourceFactory {
@ContributesBinding(RoomScope::class)
class DefaultFocusedTimelineMediaGalleryDataSourceFactory @Inject constructor(
private val room: MatrixRoom,
private val room: JoinedRoom,
private val timelineMediaItemsFactory: TimelineMediaItemsFactory,
private val mediaItemsPostProcessor: MediaItemsPostProcessor,
) : FocusedTimelineMediaGalleryDataSourceFactory {

View file

@ -12,7 +12,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
@ -40,7 +40,7 @@ interface MediaGalleryDataSource {
@SingleIn(RoomScope::class)
@ContributesBinding(RoomScope::class)
class TimelineMediaGalleryDataSource @Inject constructor(
private val room: MatrixRoom,
private val room: BaseRoom,
private val mediaTimeline: MediaTimeline,
private val timelineMediaItemsFactory: TimelineMediaItemsFactory,
private val mediaItemsPostProcessor: MediaItemsPostProcessor,

View file

@ -13,7 +13,7 @@ import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
@ -37,7 +37,7 @@ interface MediaTimeline {
@SingleIn(RoomScope::class)
@ContributesBinding(RoomScope::class)
class LiveMediaTimeline @Inject constructor(
private val room: MatrixRoom,
private val room: JoinedRoom,
) : MediaTimeline {
private var timeline: Timeline? = null
private val mutex = Mutex()
@ -62,7 +62,7 @@ class LiveMediaTimeline @Inject constructor(
* Optionally, the timeline will only contain the pinned events.
*/
class FocusedMediaTimeline(
private val room: MatrixRoom,
private val room: JoinedRoom,
private val eventId: EventId,
private val onlyPinnedEvents: Boolean,
initialMediaItem: MediaItem.Event,

View file

@ -27,7 +27,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
@ -45,7 +45,7 @@ import kotlinx.coroutines.launch
class MediaGalleryPresenter @AssistedInject constructor(
@Assisted private val navigator: MediaGalleryNavigator,
private val room: MatrixRoom,
private val room: BaseRoom,
private val mediaGalleryDataSource: MediaGalleryDataSource,
private val localMediaFactory: LocalMediaFactory,
private val mediaLoader: MatrixMediaLoader,

View file

@ -30,7 +30,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
@ -53,7 +53,7 @@ class MediaViewerPresenter @AssistedInject constructor(
@Assisted private val inputs: MediaViewerEntryPoint.Params,
@Assisted private val navigator: MediaViewerNavigator,
@Assisted private val dataSource: MediaViewerDataSource,
private val room: MatrixRoom,
private val room: JoinedRoom,
private val localMediaActions: LocalMediaActions,
) : Presenter<MediaViewerState> {
@AssistedFactory

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.mediaviewer.impl.datasource
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -18,7 +18,7 @@ class DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest {
@Test
fun `createFor should create a TimelineMediaGalleryDataSource`() = runTest {
val sut = DefaultFocusedTimelineMediaGalleryDataSourceFactory(
room = FakeMatrixRoom(),
room = FakeJoinedRoom(),
timelineMediaItemsFactory = createTimelineMediaItemsFactory(),
mediaItemsPostProcessor = MediaItemsPostProcessor(),
)

View file

@ -10,10 +10,10 @@ package io.element.android.libraries.mediaviewer.impl.datasource
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
@ -79,7 +79,7 @@ class FocusedMediaTimelineTest {
val createTimelineResult = lambdaRecorder<CreateTimelineParams, Result<Timeline>> {
Result.success(FakeTimeline())
}
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
createTimelineResult = createTimelineResult,
)
val sut = createFocusedMediaTimeline(
@ -96,7 +96,7 @@ class FocusedMediaTimelineTest {
val createTimelineResult = lambdaRecorder<CreateTimelineParams, Result<Timeline>> {
Result.success(FakeTimeline())
}
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
createTimelineResult = createTimelineResult,
)
val sut = createFocusedMediaTimeline(
@ -110,7 +110,7 @@ class FocusedMediaTimelineTest {
}
private fun createFocusedMediaTimeline(
room: MatrixRoom = FakeMatrixRoom(),
room: JoinedRoom = FakeJoinedRoom(),
eventId: EventId = AN_EVENT_ID,
initialMediaItem: MediaItem.Event = aMediaItemImage(),
onlyPinnedEvent: Boolean = false,

View file

@ -9,9 +9,9 @@ package io.element.android.libraries.mediaviewer.impl.datasource
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -31,7 +31,7 @@ class LiveMediaTimelineTest {
val createTimelineResult = lambdaRecorder<CreateTimelineParams, Result<Timeline>> {
Result.success(FakeTimeline())
}
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
createTimelineResult = createTimelineResult,
)
val sut = createLiveMediaTimeline(
@ -47,7 +47,7 @@ class LiveMediaTimelineTest {
}
private fun createLiveMediaTimeline(
room: MatrixRoom = FakeMatrixRoom(),
room: JoinedRoom = FakeJoinedRoom(),
) = LiveMediaTimeline(
room = room,
)

View file

@ -16,7 +16,7 @@ import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
@ -56,7 +56,7 @@ class TimelineMediaGalleryDataSourceTest {
fun `test - not started TimelineMediaGalleryDataSource emits no events`() = runTest {
val fakeTimeline = FakeTimeline()
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
@ -74,7 +74,7 @@ class TimelineMediaGalleryDataSourceTest {
val fakeTimeline = FakeTimeline()
runTest {
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
@ -111,7 +111,7 @@ class TimelineMediaGalleryDataSourceTest {
paginateLambda = paginateLambdaRecorder
}
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
@ -134,7 +134,7 @@ class TimelineMediaGalleryDataSourceTest {
redactEventLambda = redactEventLambdaRecorder
}
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
@ -153,7 +153,7 @@ class TimelineMediaGalleryDataSourceTest {
@Test
fun `test - failing to load timeline should emit an error`() = runTest {
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
createTimelineResult = { Result.failure(AN_EXCEPTION) },
roomCoroutineScope = backgroundScope,
)
@ -175,7 +175,7 @@ class TimelineMediaGalleryDataSourceTest {
timelineItems = timelineItems,
)
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
@ -256,7 +256,7 @@ class TimelineMediaGalleryDataSourceTest {
}
private fun TestScope.createTimelineMediaGalleryDataSource(
room: MatrixRoom = FakeMatrixRoom(
room: JoinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline(),
),
): TimelineMediaGalleryDataSource {

View file

@ -12,14 +12,15 @@ import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource
@ -51,8 +52,8 @@ class MediaGalleryPresenterTest {
mediaGalleryDataSource = FakeMediaGalleryDataSource(
startLambda = startLambda,
),
room = FakeMatrixRoom(
initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(initialRoomInfo = aRoomInfo(name = A_ROOM_NAME)),
createTimelineResult = { Result.success(FakeTimeline()) },
)
)
@ -70,8 +71,8 @@ class MediaGalleryPresenterTest {
@Test
fun `present - change mode`() = runTest {
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(initialRoomInfo = aRoomInfo(name = A_ROOM_NAME)),
createTimelineResult = { Result.success(FakeTimeline()) },
)
)
@ -99,11 +100,13 @@ class MediaGalleryPresenterTest {
private suspend fun `present - bottom sheet state - own message`(canDeleteOwn: Boolean) {
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
room = FakeJoinedRoom(
createTimelineResult = { Result.success(FakeTimeline()) },
canRedactOwnResult = { Result.success(canDeleteOwn) }
baseRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
canRedactOwnResult = { Result.success(canDeleteOwn) }
),
)
)
presenter.test {
@ -142,11 +145,13 @@ class MediaGalleryPresenterTest {
private suspend fun `present - bottom sheet state - other message`(canDeleteOther: Boolean) {
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
createTimelineResult = { Result.success(FakeTimeline()) },
canRedactOtherResult = { Result.success(canDeleteOther) }
canRedactOtherResult = { Result.success(canDeleteOther) },
),
createTimelineResult = { Result.success(FakeTimeline()) }
)
)
presenter.test {
@ -176,8 +181,8 @@ class MediaGalleryPresenterTest {
@Test
fun `present - delete bottom sheet`() = runTest {
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(initialRoomInfo = aRoomInfo(name = A_ROOM_NAME)),
createTimelineResult = { Result.success(FakeTimeline()) },
)
)
@ -244,7 +249,7 @@ class MediaGalleryPresenterTest {
onViewInTimelineClickLambda = onViewInTimelineClickLambda,
)
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
createTimelineResult = { Result.success(FakeTimeline()) },
),
navigator = navigator,
@ -284,7 +289,7 @@ class MediaGalleryPresenterTest {
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
navigator: MediaGalleryNavigator = FakeMediaGalleryNavigator(),
room: MatrixRoom = FakeMatrixRoom(
room: JoinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline(),
),
): MediaGalleryPresenter {

View file

@ -15,7 +15,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
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.timeline.item.event.toEventOrTransactionId
@ -24,7 +24,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.element.android.libraries.matrix.test.media.aMediaSource
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
@ -77,10 +78,12 @@ class MediaViewerPresenterTest {
@Test
fun `present - initial state null Event`() = runTest {
val presenter = createMediaViewerPresenter(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
canRedactOwnResult = { Result.success(true) },
)
)
)
presenter.test {
val initialState = awaitFirstItem()
assertThat(initialState.listData).isEmpty()
@ -95,10 +98,12 @@ class MediaViewerPresenterTest {
fun `present - initial state cannot show info`() = runTest {
val presenter = createMediaViewerPresenter(
canShowInfo = false,
room = FakeMatrixRoom(
room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
canRedactOwnResult = { Result.success(true) },
)
)
)
presenter.test {
val initialState = awaitFirstItem()
assertThat(initialState.listData).isEmpty()
@ -113,10 +118,12 @@ class MediaViewerPresenterTest {
fun `present - initial state Event`() = runTest {
val presenter = createMediaViewerPresenter(
eventId = AN_EVENT_ID,
room = FakeMatrixRoom(
room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
canRedactOwnResult = { Result.success(true) },
)
)
)
presenter.test {
val initialState = awaitFirstItem()
assertThat(initialState.listData).isEmpty()
@ -131,11 +138,13 @@ class MediaViewerPresenterTest {
fun `present - initial state Event from other`() = runTest {
val presenter = createMediaViewerPresenter(
eventId = AN_EVENT_ID,
room = FakeMatrixRoom(
room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
sessionId = A_SESSION_ID_2,
canRedactOtherResult = { Result.success(false) },
)
)
)
presenter.test {
val initialState = awaitFirstItem()
assertThat(initialState.listData).isEmpty()
@ -216,9 +225,9 @@ class MediaViewerPresenterTest {
)
val presenter = createMediaViewerPresenter(
mediaGalleryDataSource = mediaGalleryDataSource,
room = FakeMatrixRoom(
room = FakeJoinedRoom(baseRoom = FakeBaseRoom(
canRedactOwnResult = { Result.success(true) },
)
))
)
val anImage = aMediaItemImage(
mediaSourceUrl = aUrl,
@ -432,9 +441,9 @@ class MediaViewerPresenterTest {
startLambda = { },
)
val presenter = createMediaViewerPresenter(
room = FakeMatrixRoom(
room = FakeJoinedRoom(
liveTimeline = timeline,
canRedactOwnResult = { Result.success(true) },
baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }),
),
mediaGalleryDataSource = mediaGalleryDataSource,
mediaViewerNavigator = FakeMediaViewerNavigator(
@ -736,8 +745,8 @@ class MediaViewerPresenterTest {
)
val presenter = createMediaViewerPresenter(
mediaViewerNavigator = navigator,
room = FakeMatrixRoom(
canRedactOwnResult = { Result.success(true) },
room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }),
)
)
presenter.test {
@ -766,7 +775,7 @@ class MediaViewerPresenterTest {
),
canShowInfo: Boolean = true,
mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
room: MatrixRoom = FakeMatrixRoom(
room: JoinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline(),
),
): MediaViewerPresenter {

View file

@ -14,7 +14,7 @@ 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.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
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
@ -117,7 +117,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
return@launch
}
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
client.getRoom(roomId)?.let { room ->
client.getJoinedRoom(roomId)?.let { room ->
sendMatrixEvent(
sessionId = sessionId,
roomId = roomId,
@ -134,7 +134,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
roomId: RoomId,
threadId: ThreadId?,
replyToEventId: EventId?,
room: MatrixRoom,
room: JoinedRoom,
message: String,
) {
// Create a new event to be displayed in the notification drawer, right now

View file

@ -12,7 +12,8 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
@ -37,7 +38,7 @@ class SyncOnNotifiableEvent @Inject constructor(
}
val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@withContext
client.getRoom(notifiableEvent.roomId)?.use { room ->
client.getJoinedRoom(notifiableEvent.roomId)?.use { room ->
room.subscribeToSync()
// If the app is in foreground, sync is already running, so we just add the subscription above.
@ -60,7 +61,7 @@ class SyncOnNotifiableEvent @Inject constructor(
* User can be in the call if they answer using another session.
* If the user does not join the call, the timeout will be reached.
*/
private suspend fun MatrixRoom.waitsUntilUserIsInTheCall(timeout: Duration) {
private suspend fun BaseRoom.waitsUntilUserIsInTheCall(timeout: Duration) {
withTimeoutOrNull(timeout) {
roomInfoFlow.first {
sessionId in it.activeRoomCallParticipants
@ -68,7 +69,7 @@ class SyncOnNotifiableEvent @Inject constructor(
}
}
private suspend fun MatrixRoom.waitsUntilEventIsKnown(eventId: EventId, timeout: Duration) {
private suspend fun JoinedRoom.waitsUntilEventIsKnown(eventId: EventId, timeout: Duration) {
withTimeoutOrNull(timeout) {
liveTimeline.timelineItems.first { timelineItems ->
timelineItems.any { timelineItem ->

View file

@ -37,7 +37,7 @@ private const val A_USER_AVATAR_1 = "mxc://userAvatar1"
private const val A_USER_AVATAR_2 = "mxc://userAvatar2"
@RunWith(RobolectricTestRunner::class)
class DefaultRoomGroupMessageCreatorTest {
class DefaultBaseRoomGroupMessageCreatorTest {
@Test
fun `test createRoomMessage with one Event`() = runTest {
val sut = createRoomGroupMessageCreator()

View file

@ -27,7 +27,8 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
@ -50,6 +51,7 @@ import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -221,13 +223,13 @@ class NotificationBroadcastReceiverHandlerTest {
getLambda = getLambda
)
val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
val matrixRoom = FakeMatrixRoom()
val joinedRoom = FakeJoinedRoom()
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMessagesForRoomLambda = clearMessagesForRoomLambda,
)
val sut = createNotificationBroadcastReceiverHandler(
sessionPreferencesStore = sessionPreferencesStore,
matrixRoom = matrixRoom,
joinedRoom = joinedRoom,
notificationCleaner = fakeNotificationCleaner
)
sut.onReceive(
@ -240,7 +242,7 @@ class NotificationBroadcastReceiverHandlerTest {
clearMessagesForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
assertThat(matrixRoom.markAsReadCalls).isEqualTo(listOf(expectedReceiptType))
assertThat(joinedRoom.baseRoom.markAsReadCalls).isEqualTo(listOf(expectedReceiptType))
}
@Test
@ -292,15 +294,15 @@ class NotificationBroadcastReceiverHandlerTest {
@Test
fun `Test reject room`() = runTest {
val leaveRoom = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val matrixRoom = FakeMatrixRoom(
leaveRoomLambda = leaveRoom
val joinedRoom = FakeJoinedRoom(
baseRoom = FakeBaseRoom(leaveRoomLambda = leaveRoom),
)
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda,
)
val sut = createNotificationBroadcastReceiverHandler(
matrixRoom = matrixRoom,
joinedRoom = joinedRoom,
notificationCleaner = fakeNotificationCleaner
)
sut.onReceive(
@ -313,6 +315,9 @@ class NotificationBroadcastReceiverHandlerTest {
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
advanceUntilIdle()
leaveRoom.assertions()
.isCalledOnce()
.with()
@ -337,9 +342,9 @@ class NotificationBroadcastReceiverHandlerTest {
sendMessageLambda = sendMessage
replyMessageLambda = replyMessage
}
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeJoinedRoom(
liveTimeline = liveTimeline,
getUpdatedMemberResult = { Result.success(aRoomMember()) },
baseRoom = FakeBaseRoom(getUpdatedMemberResult = { Result.success(aRoomMember()) }),
).apply {
givenRoomInfo(
aRoomInfo(
@ -351,7 +356,7 @@ class NotificationBroadcastReceiverHandlerTest {
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
val sut = createNotificationBroadcastReceiverHandler(
matrixRoom = matrixRoom,
joinedRoom = joinedRoom,
onNotifiableEventReceived = onNotifiableEventReceived,
replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE)
)
@ -377,11 +382,11 @@ class NotificationBroadcastReceiverHandlerTest {
val liveTimeline = FakeTimeline().apply {
sendMessageLambda = sendMessage
}
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeJoinedRoom(
liveTimeline = liveTimeline
)
val sut = createNotificationBroadcastReceiverHandler(
matrixRoom = matrixRoom,
joinedRoom = joinedRoom,
replyMessageExtractor = FakeReplyMessageExtractor(" "),
)
sut.onReceive(
@ -404,9 +409,9 @@ class NotificationBroadcastReceiverHandlerTest {
sendMessageLambda = sendMessage
replyMessageLambda = replyMessage
}
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeJoinedRoom(
liveTimeline = liveTimeline,
getUpdatedMemberResult = { Result.success(aRoomMember()) },
baseRoom = FakeBaseRoom(getUpdatedMemberResult = { Result.success(aRoomMember()) }),
).apply {
givenRoomInfo(
aRoomInfo(
@ -418,7 +423,7 @@ class NotificationBroadcastReceiverHandlerTest {
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
val sut = createNotificationBroadcastReceiverHandler(
matrixRoom = matrixRoom,
joinedRoom = joinedRoom,
onNotifiableEventReceived = onNotifiableEventReceived,
replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE)
)
@ -460,10 +465,10 @@ class NotificationBroadcastReceiverHandlerTest {
}
private fun TestScope.createNotificationBroadcastReceiverHandler(
matrixRoom: FakeMatrixRoom? = FakeMatrixRoom(),
joinedRoom: FakeJoinedRoom? = FakeJoinedRoom(),
joinRoom: (RoomId) -> Result<RoomSummary?> = { lambdaError() },
matrixClient: MatrixClient? = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, matrixRoom)
givenGetRoomResult(A_ROOM_ID, joinedRoom)
joinRoomLambda = joinRoom
},
sessionPreferencesStore: SessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(),

View file

@ -18,7 +18,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
@ -49,10 +50,12 @@ class SyncOnNotifiableEventTest {
private val liveTimeline = FakeTimeline(
timelineItems = timelineItems,
)
private val room = FakeMatrixRoom(
roomId = A_ROOM_ID,
private val room = FakeJoinedRoom(
liveTimeline = liveTimeline,
subscribeToSyncLambda = subscribeToSyncLambda
baseRoom = FakeBaseRoom(
roomId = A_ROOM_ID,
subscribeToSyncLambda = subscribeToSyncLambda,
),
)
private val syncService = FakeSyncService(SyncState.Idle).also {
it.startSyncLambda = startSyncLambda

View file

@ -12,7 +12,7 @@ import io.element.android.libraries.core.hash.md5
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import java.io.File
import java.util.UUID
import javax.inject.Inject
@ -21,7 +21,7 @@ import javax.inject.Inject
class DefaultVoiceFileManager @Inject constructor(
@CacheDirectory private val cacheDir: File,
private val config: VoiceFileConfig,
room: MatrixRoom,
room: BaseRoom,
) : VoiceFileManager {
private val roomId: RoomId = room.roomId