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

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