From f56ccdc7869c8cb5020063d13aee5f592a39d6c2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 5 Sep 2025 10:27:33 +0200 Subject: [PATCH] feature(spaces) : introduce SpaceRoomList matrix api --- .../home/impl/spaces/HomeSpacesPresenter.kt | 5 +- .../home/impl/spaces/HomeSpacesState.kt | 3 +- .../impl/spaces/HomeSpacesStateProvider.kt | 14 +-- .../home/impl/spaces/HomeSpacesView.kt | 9 +- .../home/impl/spaces/SpaceRoomProvider.kt | 26 ++--- .../features/invite/api/SeenInvitesStore.kt | 6 -- .../libraries/matrix/api/spaces/SpaceRoom.kt | 4 +- .../matrix/api/spaces/SpaceRoomList.kt | 23 +++++ .../matrix/api/spaces/SpaceService.kt | 5 +- .../matrix/impl/spaces/RustSpaceRoomList.kt | 56 +++++++++++ .../matrix/impl/spaces/RustSpaceService.kt | 96 ++++--------------- .../impl/spaces/SpaceListUpdateProcessor.kt | 86 +++++++++++++++++ .../impl/spaces/SpaceRoomListExtensions.kt | 54 +++++++++++ .../matrix/impl/spaces/SpaceRoomMapper.kt | 3 +- .../matrix/ui/model/SpaceExtension.kt | 2 +- 15 files changed, 277 insertions(+), 115 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomListExtensions.kt diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt index 29ceca14bf..b29da29a94 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import dev.zacsweers.metro.Inject import io.element.android.features.invite.api.SeenInvitesStore -import io.element.android.features.invite.api.seenSpaceIds import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.matrix.api.MatrixClient @@ -34,9 +33,9 @@ class HomeSpacesPresenter( .mediaPreviewConfigFlow .mapState { config -> config.hideInviteAvatar } }.collectAsState() - val spaceRooms by client.spaceService.spaceRooms.collectAsState(emptyList()) + val spaceRooms by client.spaceService.spaceRoomsFlow.collectAsState(emptyList()) val seenSpaceInvites by remember { - seenInvitesStore.seenSpaceIds().map { it.toPersistentSet() } + seenInvitesStore.seenRoomIds().map { it.toPersistentSet() } }.collectAsState(persistentSetOf()) fun handleEvents(event: HomeSpacesEvents) { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt index 6def46c7b3..39871fb905 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt @@ -7,6 +7,7 @@ package io.element.android.features.home.impl.spaces +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.spaces.SpaceRoom import kotlinx.collections.immutable.ImmutableSet @@ -14,7 +15,7 @@ import kotlinx.collections.immutable.ImmutableSet data class HomeSpacesState( val space: CurrentSpace, val spaceRooms: List, - val seenSpaceInvites: ImmutableSet, + val seenSpaceInvites: ImmutableSet, val hideInvitesAvatar: Boolean, val eventSink: (HomeSpacesEvents) -> Unit, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt index 466f515b26..1aedc0ef19 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt @@ -8,7 +8,7 @@ package io.element.android.features.home.impl.spaces import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.api.core.SpaceId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.spaces.SpaceRoom import kotlinx.collections.immutable.toImmutableSet @@ -18,12 +18,12 @@ open class HomeSpacesStateProvider : PreviewParameterProvider { aHomeSpacesState( spaceRooms = SpaceRoomProvider().values.toList(), seenSpaceInvites = setOf( - SpaceId("!spaceId3:example.com"), + RoomId("!spaceId3:example.com"), ), ), aHomeSpacesState( space = CurrentSpace.Space( - spaceRoom = aSpaceRooms(spaceId = SpaceId("!mySpace:example.com")) + spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com")) ), spaceRooms = aListOfSpaceRooms(), ), @@ -33,7 +33,7 @@ open class HomeSpacesStateProvider : PreviewParameterProvider { internal fun aHomeSpacesState( space: CurrentSpace = CurrentSpace.Root, spaceRooms: List = aListOfSpaceRooms(), - seenSpaceInvites: Set = emptySet(), + seenSpaceInvites: Set = emptySet(), hideInvitesAvatar: Boolean = false, eventSink: (HomeSpacesEvents) -> Unit = {}, ) = HomeSpacesState( @@ -46,8 +46,8 @@ internal fun aHomeSpacesState( fun aListOfSpaceRooms(): List { return listOf( - aSpaceRooms(spaceId = SpaceId("!spaceId0:example.com")), - aSpaceRooms(spaceId = SpaceId("!spaceId1:example.com")), - aSpaceRooms(spaceId = SpaceId("!spaceId2:example.com")), + aSpaceRoom(roomId = RoomId("!spaceId0:example.com")), + aSpaceRoom(roomId = RoomId("!spaceId1:example.com")), + aSpaceRoom(roomId = RoomId("!spaceId2:example.com")), ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt index be6639e94b..ae1cf09751 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView @@ -24,7 +25,7 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun HomeSpacesView( state: HomeSpacesState, - onSpaceClick: (SpaceId) -> Unit, + onSpaceClick: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn(modifier) { @@ -52,14 +53,14 @@ fun HomeSpacesView( } } state.spaceRooms.forEach { - item(it.spaceId) { + item(it.roomId) { val isInvitation = it.state == CurrentUserMembership.INVITED HomeSpaceItemView( spaceRoom = it, - showUnreadIndicator = isInvitation && it.spaceId !in state.seenSpaceInvites, + showUnreadIndicator = isInvitation && it.roomId !in state.seenSpaceInvites, hideAvatars = isInvitation && state.hideInvitesAvatar, onClick = { - onSpaceClick(it.spaceId) + onSpaceClick(it.roomId) } ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt index 29c18c78a0..88c3c5799a 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt @@ -9,7 +9,7 @@ package io.element.android.features.home.impl.spaces import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.SpaceId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.join.JoinRule @@ -18,42 +18,42 @@ import io.element.android.libraries.matrix.api.user.MatrixUser class SpaceRoomProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( - aSpaceRooms(), - aSpaceRooms( + aSpaceRoom(), + aSpaceRoom( numJoinedMembers = 5, childrenCount = 10, worldReadable = true, - spaceId = SpaceId("!spaceId0:example.com"), + roomId = RoomId("!spaceId0:example.com"), ), - aSpaceRooms( + aSpaceRoom( numJoinedMembers = 5, childrenCount = 10, worldReadable = true, avatarUrl = "anUrl", - spaceId = SpaceId("!spaceId1:example.com"), + roomId = RoomId("!spaceId1:example.com"), ), - aSpaceRooms( + aSpaceRoom( name = null, numJoinedMembers = 5, childrenCount = 10, worldReadable = true, avatarUrl = "anUrl", - spaceId = SpaceId("!spaceId2:example.com"), + roomId = RoomId("!spaceId2:example.com"), state = CurrentUserMembership.INVITED, ), - aSpaceRooms( + aSpaceRoom( name = null, numJoinedMembers = 5, childrenCount = 10, worldReadable = true, avatarUrl = "anUrl", - spaceId = SpaceId("!spaceId3:example.com"), + roomId = RoomId("!spaceId3:example.com"), state = CurrentUserMembership.INVITED, ), ) } -fun aSpaceRooms( +fun aSpaceRoom( name: String? = "Space name", avatarUrl: String? = null, canonicalAlias: RoomAlias? = null, @@ -62,7 +62,7 @@ fun aSpaceRooms( heroes: List = emptyList(), joinRule: JoinRule? = null, numJoinedMembers: Int = 0, - spaceId: SpaceId = SpaceId("!spaceId:example.com"), + roomId: RoomId = RoomId("!roomId:example.com"), roomType: RoomType = RoomType.Space, state: CurrentUserMembership? = null, topic: String? = null, @@ -76,7 +76,7 @@ fun aSpaceRooms( heroes = heroes, joinRule = joinRule, numJoinedMembers = numJoinedMembers, - spaceId = spaceId, + roomId = roomId, roomType = roomType, state = state, topic = topic, diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt index 6c609e3810..283deb78fb 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt @@ -38,9 +38,3 @@ interface SeenInvitesStore { */ suspend fun clear() } - -fun SeenInvitesStore.seenSpaceIds(): Flow> { - return seenRoomIds().map { roomIds -> - roomIds.map { it.toSpaceId() }.toSet() - } -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt index ce697089c7..102e594ea7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.matrix.api.spaces import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.SpaceId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.join.JoinRule @@ -23,7 +23,7 @@ data class SpaceRoom( val heroes: List, val joinRule: JoinRule?, val numJoinedMembers: Int, - val spaceId: SpaceId, + val roomId: RoomId, val roomType: RoomType, val state: CurrentUserMembership?, val topic: String?, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt new file mode 100644 index 0000000000..a1f79cdb0b --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt @@ -0,0 +1,23 @@ +/* + * 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.spaces + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface SpaceRoomList { + sealed interface PaginationStatus { + data object Loading : PaginationStatus + data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus + } + + val spaceRoomsFlow: Flow> + val paginationStatusFlow: StateFlow + suspend fun paginate(): Result +} + diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt index 58494502f7..917369a6a7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt @@ -7,9 +7,12 @@ package io.element.android.libraries.matrix.api.spaces +import io.element.android.libraries.matrix.api.core.SpaceId import kotlinx.coroutines.flow.SharedFlow interface SpaceService { - val spaceRooms: SharedFlow> + val spaceRoomsFlow: SharedFlow> suspend fun joinedSpaces(): Result> + + suspend fun spaceRoomList(spaceId: SpaceId): SpaceRoomList } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt new file mode 100644 index 0000000000..efe405714e --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.spaces + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState +import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList + +class RustSpaceRoomList( + private val inner: InnerSpaceRoomList, + sessionCoroutineScope: CoroutineScope, + spaceRoomMapper: SpaceRoomMapper, +) : SpaceRoomList { + override val spaceRoomsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = Int.MAX_VALUE) + override val paginationStatusFlow = MutableStateFlow(inner.paginationState().into()) + private val spaceListUpdateProcessor = SpaceListUpdateProcessor(spaceRoomsFlow, spaceRoomMapper) + + init { + inner.paginationStateFlow() + .onEach { paginationStatus -> + paginationStatusFlow.emit(paginationStatus.into()) + } + .launchIn(sessionCoroutineScope) + + inner.spaceListUpdateFlow() + .onEach { updates -> + spaceListUpdateProcessor.postUpdates(updates) + } + .launchIn(sessionCoroutineScope) + } + + override suspend fun paginate(): Result { + return runCatchingExceptions { + inner.paginate() + } + } + + private fun SpaceRoomListPaginationState.into(): SpaceRoomList.PaginationStatus { + return when (this) { + is SpaceRoomListPaginationState.Idle -> SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = !endReached) + SpaceRoomListPaginationState.Loading -> SpaceRoomList.PaginationStatus.Loading + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt index b059d476bc..4d26040021 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt @@ -8,7 +8,9 @@ package io.element.android.libraries.matrix.impl.spaces import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import kotlinx.coroutines.CoroutineDispatcher @@ -21,11 +23,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.SpaceListUpdate import org.matrix.rustcomponents.sdk.SpaceServiceInterface @@ -38,104 +37,49 @@ class RustSpaceService( private val sessionCoroutineScope: CoroutineScope, private val sessionDispatcher: CoroutineDispatcher, ) : SpaceService { - private val mapper = SpaceRoomMapper() - private val mutex = Mutex() - - override val spaceRooms = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + private val spaceRoomMapper = SpaceRoomMapper() + override val spaceRoomsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + private val spaceListUpdateProcessor = SpaceListUpdateProcessor(spaceRoomsFlow, spaceRoomMapper) override suspend fun joinedSpaces(): Result> = withContext(sessionDispatcher) { runCatchingExceptions { innerSpaceService.joinedSpaces() .map { - it.let(mapper::map) + it.let(spaceRoomMapper::map) } } } - // override suspend fun spaceRoomList(spaceId: SpaceId): Result> = withContext(sessionDispatcher) { - // runCatchingExceptions { - // innerSpaceService.spaceRoomList(spaceId.value) - // } - // } + override suspend fun spaceRoomList(spaceId: SpaceId): SpaceRoomList { + val innerSpaceRoomList = innerSpaceService.spaceRoomList(spaceId.value) + return RustSpaceRoomList( + inner = innerSpaceRoomList, + sessionCoroutineScope = sessionCoroutineScope, + spaceRoomMapper = spaceRoomMapper + ) + } init { innerSpaceService - .spaceDiffFlow() - .onEach { - handeUpdate(it) + .spaceListUpdate() + .onEach { updates -> + spaceListUpdateProcessor.postUpdates(updates) } .launchIn(sessionCoroutineScope) } - - private suspend fun handeUpdate(spaceListUpdates: List) { - mutex.withLock { - val current = if (spaceRooms.replayCache.isNotEmpty()) { - spaceRooms.first().toMutableList() - } else { - mutableListOf() - } - spaceListUpdates.forEach { update -> - current.applyUpdate(update) - } - spaceRooms.emit(current) - } - } - - private fun MutableList.applyUpdate(update: SpaceListUpdate) { - when (update) { - is SpaceListUpdate.Append -> { - val newSpaces = update.values.map(mapper::map) - addAll(newSpaces) - } - SpaceListUpdate.Clear -> clear() - is SpaceListUpdate.Insert -> { - val newSpace = mapper.map(update.value) - add(update.index.toInt(), newSpace) - } - SpaceListUpdate.PopBack -> { - removeAt(lastIndex) - } - SpaceListUpdate.PopFront -> { - removeAt(0) - } - is SpaceListUpdate.PushBack -> { - val newSpace = mapper.map(update.value) - add(newSpace) - } - is SpaceListUpdate.PushFront -> { - val newSpace = mapper.map(update.value) - add(0, newSpace) - } - is SpaceListUpdate.Remove -> { - removeAt(update.index.toInt()) - } - is SpaceListUpdate.Reset -> { - clear() - val newSpaces = update.values.map(mapper::map) - addAll(newSpaces) - } - is SpaceListUpdate.Set -> { - val newSpace = mapper.map(update.value) - this[update.index.toInt()] = newSpace - } - is SpaceListUpdate.Truncate -> { - subList(update.length.toInt(), size).clear() - } - } - } } -internal fun SpaceServiceInterface.spaceDiffFlow(): Flow> = +internal fun SpaceServiceInterface.spaceListUpdate(): Flow> = callbackFlow { val listener = object : SpaceServiceJoinedSpacesListener { override fun onUpdate(roomUpdates: List) { trySendBlocking(roomUpdates) } } - Timber.d("Open spaceDiffFlow for SpaceServiceInterface ${this@spaceDiffFlow}") + Timber.d("Open spaceDiffFlow for SpaceServiceInterface ${this@spaceListUpdate}") val taskHandle = subscribeToJoinedSpaces(listener) awaitClose { - Timber.d("Close spaceDiffFlow for SpaceServiceInterface ${this@spaceDiffFlow}") + Timber.d("Close spaceDiffFlow for SpaceServiceInterface ${this@spaceListUpdate}") taskHandle.cancelAndDestroy() } }.catch { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt new file mode 100644 index 0000000000..a1f8584299 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt @@ -0,0 +1,86 @@ +/* + * 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.spaces + +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.matrix.rustcomponents.sdk.SpaceListUpdate +import timber.log.Timber + +internal class SpaceListUpdateProcessor( + private val spaceRoomsFlow: MutableSharedFlow>, + private val mapper: SpaceRoomMapper, +) { + private val mutex = Mutex() + + suspend fun postUpdates(updates: List) { + Timber.v("Update space rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}") + updateSpaceRooms { + updates.forEach { update -> applyUpdate(update) } + } + } + + private suspend fun updateSpaceRooms(block: MutableList.() -> Unit) = + mutex.withLock { + val spaceRooms = if (spaceRoomsFlow.replayCache.isNotEmpty()) { + spaceRoomsFlow.first().toMutableList() + } else { + mutableListOf() + } + block(spaceRooms) + spaceRoomsFlow.tryEmit(spaceRooms) + } + + private fun MutableList.applyUpdate(update: SpaceListUpdate) { + when (update) { + is SpaceListUpdate.Append -> { + val newSpaces = update.values.map { it -> + it.let(mapper::map) + } + addAll(newSpaces) + } + SpaceListUpdate.Clear -> clear() + is SpaceListUpdate.Insert -> { + val newSpace = mapper.map(update.value) + add(update.index.toInt(), newSpace) + } + SpaceListUpdate.PopBack -> { + removeAt(lastIndex) + } + SpaceListUpdate.PopFront -> { + removeAt(0) + } + is SpaceListUpdate.PushBack -> { + val newSpace = mapper.map(update.value) + add(newSpace) + } + is SpaceListUpdate.PushFront -> { + val newSpace = mapper.map(update.value) + add(0, newSpace) + } + is SpaceListUpdate.Remove -> { + removeAt(update.index.toInt()) + } + is SpaceListUpdate.Reset -> { + clear() + val newSpaces = update.values.map(mapper::map) + addAll(newSpaces) + } + is SpaceListUpdate.Set -> { + val newSpace = mapper.map(update.value) + this[update.index.toInt()] = newSpace + } + is SpaceListUpdate.Truncate -> { + subList(update.length.toInt(), size).clear() + } + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomListExtensions.kt new file mode 100644 index 0000000000..4976f37117 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomListExtensions.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.spaces + +import io.element.android.libraries.matrix.impl.util.cancelAndDestroy +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import org.matrix.rustcomponents.sdk.SpaceListUpdate +import org.matrix.rustcomponents.sdk.SpaceRoomListEntriesListener +import org.matrix.rustcomponents.sdk.SpaceRoomListInterface +import org.matrix.rustcomponents.sdk.SpaceRoomListPaginationStateListener +import timber.log.Timber +import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState + +internal fun SpaceRoomListInterface.paginationStateFlow(): Flow = callbackFlow { + val listener = object : SpaceRoomListPaginationStateListener { + override fun onUpdate(paginationState: SpaceRoomListPaginationState) { + trySend(paginationState) + } + } + val result = subscribeToPaginationStateUpdates(listener) + awaitClose { + result.cancelAndDestroy() + } +}.catch { + Timber.d(it, "paginationStateFlow() failed") +}.buffer(Channel.UNLIMITED) + +internal fun SpaceRoomListInterface.spaceListUpdateFlow(): Flow> = + callbackFlow { + val listener = object : SpaceRoomListEntriesListener { + override fun onUpdate(rooms: List) { + trySendBlocking(rooms) + } + } + Timber.d("Open spaceListUpdateFlow for SpaceRoomListInterface ${this@spaceListUpdateFlow}") + val taskHandle = subscribeToRoomUpdate(listener) + awaitClose { + Timber.d("Close spaceListUpdateFlow for SpaceRoomListInterface ${this@spaceListUpdateFlow}") + taskHandle.cancelAndDestroy() + } + }.catch { + Timber.d(it, "spaceListUpdateFlow() failed") + }.buffer(Channel.UNLIMITED) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt index 5622ee2c8c..ade5063515 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.spaces import io.element.android.libraries.core.bool.orFalse 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.SpaceId import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.impl.room.join.map @@ -26,7 +27,7 @@ class SpaceRoomMapper { joinRule = spaceRoom.joinRule?.map(), name = spaceRoom.name, numJoinedMembers = spaceRoom.numJoinedMembers.toInt(), - spaceId = spaceRoom.roomId.let(::SpaceId), + roomId = RoomId(spaceRoom.roomId), roomType = spaceRoom.roomType.map(), state = spaceRoom.state?.map(), topic = spaceRoom.topic, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.kt index afe03b6877..e4b056fdea 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.kt @@ -12,7 +12,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.spaces.SpaceRoom fun SpaceRoom.getAvatarData(size: AvatarSize) = AvatarData( - id = spaceId.value, + id = roomId.value, name = name, url = avatarUrl, size = size,