feature (space) : add space cache and navigation to sub space/room

This commit is contained in:
ganfra 2025-09-10 10:48:34 +02:00 committed by Benoit Marty
parent b45a4c3b2c
commit d4d2aa1707
20 changed files with 278 additions and 101 deletions

View file

@ -16,6 +16,8 @@ interface SpaceRoomList {
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
}
fun currentSpaceFlow(): Flow<SpaceRoom?>
val spaceRoomsFlow: Flow<List<SpaceRoom>>
val paginationStatusFlow: StateFlow<PaginationStatus>
suspend fun paginate(): Result<Unit>

View file

@ -8,10 +8,12 @@
package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
@ -21,16 +23,27 @@ import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomList(
private val roomId: RoomId,
private val innerProvider: suspend () -> InnerSpaceRoomList,
sessionCoroutineScope: CoroutineScope,
spaceRoomMapper: SpaceRoomMapper,
private val spaceRoomCache: SpaceRoomCache,
) : SpaceRoomList {
private val inner = CompletableDeferred<InnerSpaceRoomList>()
override fun currentSpaceFlow(): Flow<SpaceRoom?> {
return spaceRoomCache.getSpaceRoomFlow(roomId)
}
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
override val paginationStatusFlow: MutableStateFlow<SpaceRoomList.PaginationStatus> =
MutableStateFlow(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(spaceRoomsFlow, spaceRoomMapper)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
mapper = spaceRoomMapper,
spaceRoomCache = spaceRoomCache
)
init {
sessionCoroutineScope.launch {

View file

@ -39,8 +39,13 @@ class RustSpaceService(
private val sessionDispatcher: CoroutineDispatcher,
) : SpaceService {
private val spaceRoomMapper = SpaceRoomMapper()
private val spaceRoomCache = SpaceRoomCache()
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(spaceRoomsFlow, spaceRoomMapper)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
mapper = spaceRoomMapper,
spaceRoomCache = spaceRoomCache
)
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions {
@ -53,9 +58,11 @@ class RustSpaceService(
override fun spaceRoomList(id: RoomId): SpaceRoomList {
return RustSpaceRoomList(
roomId = id,
innerProvider = { innerSpaceService.spaceRoomList(id.value) },
sessionCoroutineScope = sessionCoroutineScope,
spaceRoomMapper = spaceRoomMapper
spaceRoomMapper = spaceRoomMapper,
spaceRoomCache = spaceRoomCache,
)
}

View file

@ -18,6 +18,7 @@ import timber.log.Timber
internal class SpaceListUpdateProcessor(
private val spaceRoomsFlow: MutableSharedFlow<List<SpaceRoom>>,
private val mapper: SpaceRoomMapper,
private val spaceRoomCache: SpaceRoomCache,
) {
private val mutex = Mutex()
@ -36,7 +37,8 @@ internal class SpaceListUpdateProcessor(
mutableListOf()
}
block(spaceRooms)
spaceRoomsFlow.tryEmit(spaceRooms)
spaceRoomCache.update(spaceRooms)
spaceRoomsFlow.emit(spaceRooms)
}
private fun MutableList<SpaceRoom>.applyUpdate(update: SpaceListUpdate) {

View file

@ -0,0 +1,44 @@
/*
* 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 dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* An in memory cache of space rooms.
* Only caches Rooms with roomType [io.element.android.libraries.matrix.api.room.RoomType.Space].
*/
class SpaceRoomCache() {
private val inMemoryCache = MutableStateFlow<MutableMap<RoomId, SpaceRoom>>(LinkedHashMap())
private val mutex = Mutex()
fun getSpaceRoomFlow(roomId: RoomId): Flow<SpaceRoom?> {
return inMemoryCache.map { it[roomId] }
}
suspend fun update(spaceRooms: List<SpaceRoom>) = mutex.withLock {
inMemoryCache.update { cache ->
spaceRooms
.filter { it.isSpace }
.forEach { spaceRoom ->
cache.put(spaceRoom.roomId, spaceRoom)
}
cache
}
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.previewutils.room
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.user.MatrixUser
fun aSpaceRoom(
name: String? = "Space name",
avatarUrl: String? = null,
canonicalAlias: RoomAlias? = null,
childrenCount: Int = 0,
guestCanJoin: Boolean = false,
heroes: List<MatrixUser> = emptyList(),
joinRule: JoinRule? = null,
numJoinedMembers: Int = 0,
roomId: RoomId = RoomId("!roomId:example.com"),
roomType: RoomType = RoomType.Space,
state: CurrentUserMembership? = null,
topic: String? = null,
worldReadable: Boolean = false,
) = SpaceRoom(
name = name,
avatarUrl = avatarUrl,
canonicalAlias = canonicalAlias,
childrenCount = childrenCount,
guestCanJoin = guestCanJoin,
heroes = heroes,
joinRule = joinRule,
numJoinedMembers = numJoinedMembers,
roomId = roomId,
roomType = roomType,
state = state,
topic = topic,
worldReadable = worldReadable
)