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 6b9afd9e6f
commit 65961ad404
20 changed files with 278 additions and 101 deletions

View File

@@ -60,6 +60,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.features.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
@@ -334,7 +335,7 @@ class LoggedInFlowNode(
.build()
}
is NavTarget.Room -> {
val callback = object : JoinedRoomLoadedFlowNode.Callback {
val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
override fun onOpenRoom(roomId: RoomId, serverNames: List<String>) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames))
}
@@ -373,6 +374,11 @@ class LoggedInFlowNode(
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
}
}
val spaceCallback = object : SpaceEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
}
val inputs = RoomFlowNode.Inputs(
roomIdOrAlias = navTarget.roomIdOrAlias,
roomDescription = Optional.ofNullable(navTarget.roomDescription),
@@ -380,7 +386,7 @@ class LoggedInFlowNode(
trigger = Optional.ofNullable(navTarget.trigger),
initialElement = navTarget.initialElement
)
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, joinedRoomCallback, spaceCallback))
}
is NavTarget.UserProfile -> {
val callback = object : UserProfileEntryPoint.Callback {

View File

@@ -45,7 +45,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
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.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
@@ -216,8 +215,10 @@ class RoomFlowNode(
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
}
is NavTarget.Space -> {
val spaceCallback = plugins<SpaceEntryPoint.Callback>().single()
spaceEntryPoint.nodeBuilder(this, buildContext)
.params(SpaceEntryPoint.Params.Id(navTarget.spaceId))
.inputs(SpaceEntryPoint.Inputs(roomId = navTarget.spaceId))
.callback(spaceCallback)
.build()
}
}

View File

@@ -55,6 +55,7 @@ dependencies {
implementation(libs.haze.materials)
implementation(projects.features.reportroom.api)
implementation(projects.features.changeroommemberroles.api)
implementation(projects.libraries.previewutils)
api(projects.features.home.api)
testImplementation(libs.androidx.compose.ui.test.junit)

View File

@@ -10,6 +10,7 @@ package io.element.android.features.home.impl.spaces
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableSet
open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {

View File

@@ -8,13 +8,10 @@
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.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
import io.element.android.libraries.previewutils.room.aSpaceRoom
class SpaceRoomProvider : PreviewParameterProvider<SpaceRoom> {
override val values: Sequence<SpaceRoom> = sequenceOf(
@@ -52,33 +49,3 @@ class SpaceRoomProvider : PreviewParameterProvider<SpaceRoom> {
),
)
}
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
)

View File

@@ -12,8 +12,6 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
interface SpaceEntryPoint : FeatureEntryPoint {
fun nodeBuilder(
@@ -22,24 +20,16 @@ interface SpaceEntryPoint : FeatureEntryPoint {
): NodeBuilder
interface NodeBuilder {
fun params(params: Params): NodeBuilder
fun inputs(inputs: Inputs): NodeBuilder
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
sealed interface Params : Plugin {
data class Id(val roomId: RoomId) : Params
data class Full(val spaceRoom: SpaceRoom) : Params
fun roomId(): RoomId {
return when (this) {
is Id -> roomId
is Full -> spaceRoom.roomId
}
}
}
data class Inputs(
val roomId: RoomId
) : Plugin
interface Callback : Plugin {
fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>)
fun onOpenRoom(roomId: RoomId)
}
}

View File

@@ -38,6 +38,7 @@ dependencies {
implementation(libs.coil.compose)
implementation(projects.libraries.featureflag.api)
implementation(projects.features.invite.api)
implementation(projects.libraries.previewutils)
api(projects.features.space.api)
testImplementation(libs.test.junit)

View File

@@ -21,8 +21,8 @@ class DefaultSpaceEntryPoint @Inject constructor() : SpaceEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SpaceEntryPoint.NodeBuilder {
val plugins = mutableSetOf<Plugin>()
return object : SpaceEntryPoint.NodeBuilder {
override fun params(params: SpaceEntryPoint.Params): SpaceEntryPoint.NodeBuilder {
plugins.add(params)
override fun inputs(inputs: SpaceEntryPoint.Inputs): SpaceEntryPoint.NodeBuilder {
plugins.add(inputs)
return this
}

View File

@@ -8,5 +8,5 @@
package io.element.android.features.space.impl
sealed interface SpaceEvents {
data object LoadMore : SpaceEvents
}

View File

@@ -22,11 +22,12 @@ import io.element.android.libraries.di.SessionScope
class SpaceNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenterFactory: SpacePresenter.Factory,
presenterFactory: SpacePresenter.Factory,
) : Node(buildContext, plugins = plugins) {
val params = plugins.filterIsInstance<SpaceEntryPoint.Params>().single()
private val presenter = presenterFactory.create(params)
val inputs = plugins.filterIsInstance<SpaceEntryPoint.Inputs>().single()
val callback = plugins.filterIsInstance<SpaceEntryPoint.Callback>().single()
private val presenter = presenterFactory.create(inputs)
@Composable
override fun View(modifier: Modifier) {
@@ -34,6 +35,9 @@ class SpaceNode @AssistedInject constructor(
SpaceView(
state = state,
onBackClick = ::navigateUp,
onRoomClick = { roomId ->
callback.onOpenRoom(roomId)
},
modifier = modifier
)
}

View File

@@ -8,9 +8,11 @@
package io.element.android.features.space.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
@@ -19,27 +21,35 @@ import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@Inject
class SpacePresenter(
@Assisted private val params: SpaceEntryPoint.Params,
@Assisted private val inputs: SpaceEntryPoint.Inputs,
private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore,
) : Presenter<SpaceState> {
@AssistedFactory
interface Factory {
fun create(params: SpaceEntryPoint.Params): SpacePresenter
fun create(inputs: SpaceEntryPoint.Inputs): SpacePresenter
}
private val spaceRoomList = client.spaceService.spaceRoomList(params.roomId())
private val spaceRoomList = client.spaceService.spaceRoomList(inputs.roomId)
@Composable
override fun present(): SpaceState {
LaunchedEffect(Unit) {
paginate()
}
val hideInvitesAvatar by remember {
client
.mediaPreviewService()
@@ -50,18 +60,35 @@ class SpacePresenter(
seenInvitesStore.seenRoomIds().map { it.toPersistentSet() }
}.collectAsState(persistentSetOf())
val coroutineScope = rememberCoroutineScope()
val children by spaceRoomList.spaceRoomsFlow.collectAsState(emptyList())
val hasMoreToLoad by remember {
spaceRoomList.paginationStatusFlow.mapState { status ->
when (status) {
is SpaceRoomList.PaginationStatus.Idle -> status.hasMoreToLoad
SpaceRoomList.PaginationStatus.Loading -> true
}
}
}.collectAsState()
val currentSpace by remember { spaceRoomList.currentSpaceFlow() }.collectAsState(null)
fun handleEvents(event: SpaceEvents) {
//when (event) { }
when (event) {
SpaceEvents.LoadMore -> coroutineScope.paginate()
}
}
return SpaceState(
parentSpace = null,
currentSpace = currentSpace,
children = children.toPersistentList(),
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
hasMoreToLoad = hasMoreToLoad,
eventSink = ::handleEvents,
)
}
private fun CoroutineScope.paginate() = launch {
spaceRoomList.paginate()
}
}

View File

@@ -13,9 +13,10 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
data class SpaceState(
val parentSpace: SpaceRoom?,
val currentSpace: SpaceRoom?,
val children: ImmutableList<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val hasMoreToLoad: Boolean,
val eventSink: (SpaceEvents) -> Unit
)

View File

@@ -8,18 +8,53 @@
package io.element.android.features.space.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import kotlinx.collections.immutable.persistentListOf
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
override val values: Sequence<SpaceState>
get() = sequenceOf(
aSpaceState(),
aSpaceState(hasMoreToLoad = true),
aSpaceState(
hasMoreToLoad = true,
children = aListOfSpaceRooms(),
),
aSpaceState(
hasMoreToLoad = false,
children = aListOfSpaceRooms()
)
// Add other states here
)
}
fun aSpaceState() = SpaceState(
parentSpace = null,
children = persistentListOf(),
fun aSpaceState(
parentSpace: SpaceRoom? = aSpaceRoom(
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
roomId = RoomId("!spaceId0:example.com"),
),
children: List<SpaceRoom> = emptyList(),
seenSpaceInvites: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false,
hasMoreToLoad: Boolean = false,
) = SpaceState(
currentSpace = parentSpace,
children = children.toImmutableList(),
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar,
hasMoreToLoad = hasMoreToLoad,
eventSink = {}
)
private fun aListOfSpaceRooms(): List<SpaceRoom> {
return listOf(
aSpaceRoom(roomId = RoomId("!spaceId0:example.com")),
aSpaceRoom(roomId = RoomId("!spaceId1:example.com")),
aSpaceRoom(roomId = RoomId("!spaceId2:example.com")),
)
}

View File

@@ -9,13 +9,15 @@ package io.element.android.features.space.impl
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -33,12 +35,13 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
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.spaces.SpaceRoom
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
import io.element.android.libraries.matrix.ui.model.getAvatarData
@@ -49,56 +52,57 @@ import kotlinx.collections.immutable.toImmutableList
fun SpaceView(
state: SpaceState,
onBackClick: () -> Unit,
onRoomClick: (roomId: RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets.statusBars,
topBar = {
SpaceViewTopBar(spaceRoom = null, onBackClick = onBackClick)
},
content = { padding ->
Box(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
modifier = Modifier.padding(padding)
) {
SpaceViewContent(state)
SpaceViewContent(
state = state,
onRoomClick = onRoomClick
)
}
},
)
}
@Composable
private fun SpaceViewContent(
state: SpaceState,
onRoomClick: (roomId: RoomId) -> Unit,
modifier: Modifier = Modifier,
){
LazyColumn(modifier) {
val parentSpace = state.parentSpace
if (parentSpace != null) {
) {
LazyColumn(modifier.fillMaxSize()) {
val currentSpace = state.currentSpace
if (currentSpace != null) {
item {
SpaceHeaderView(
avatarData = parentSpace.getAvatarData(AvatarSize.SpaceHeader),
name = parentSpace.name,
topic = parentSpace.topic,
joinRule = parentSpace.joinRule,
heroes = parentSpace.heroes.toImmutableList(),
numberOfMembers = parentSpace.numJoinedMembers,
numberOfRooms = parentSpace.childrenCount,
avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader),
name = currentSpace.name,
topic = currentSpace.topic,
joinRule = currentSpace.joinRule,
heroes = currentSpace.heroes.toImmutableList(),
numberOfMembers = currentSpace.numJoinedMembers,
numberOfRooms = currentSpace.childrenCount,
)
}
}
state.children.forEach {
item(it.roomId) {
val isInvitation = it.state == CurrentUserMembership.INVITED
state.children.forEach { spaceRoom ->
item {
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
SpaceRoomItemView(
spaceRoom = it,
showUnreadIndicator = isInvitation && it.roomId !in state.seenSpaceInvites,
spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = {
onRoomClick(spaceRoom.roomId)
},
onLongClick = {
@@ -106,9 +110,33 @@ private fun SpaceViewContent(
)
}
}
if (state.hasMoreToLoad) {
item {
LoadingMoreIndicator(eventSink = state.eventSink)
}
}
}
}
@Composable
private fun LoadingMoreIndicator(
eventSink: (SpaceEvents) -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
strokeWidth = 2.dp,
modifier = Modifier.padding(vertical = 8.dp)
)
val latestEventSink by rememberUpdatedState(eventSink)
LaunchedEffect(Unit) {
latestEventSink(SpaceEvents.LoadMore)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -171,6 +199,7 @@ internal fun SpaceViewPreview(
) = ElementPreview {
SpaceView(
state = state,
onRoomClick = {},
onBackClick = {},
)
}

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
)