feature(spaces) : introduce SpaceRoomList matrix api

This commit is contained in:
ganfra 2025-09-05 10:27:33 +02:00 committed by Benoit Marty
parent 47ca80a939
commit f56ccdc786
15 changed files with 277 additions and 115 deletions

View file

@ -13,7 +13,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.SeenInvitesStore 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.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -34,9 +33,9 @@ class HomeSpacesPresenter(
.mediaPreviewConfigFlow .mediaPreviewConfigFlow
.mapState { config -> config.hideInviteAvatar } .mapState { config -> config.hideInviteAvatar }
}.collectAsState() }.collectAsState()
val spaceRooms by client.spaceService.spaceRooms.collectAsState(emptyList()) val spaceRooms by client.spaceService.spaceRoomsFlow.collectAsState(emptyList())
val seenSpaceInvites by remember { val seenSpaceInvites by remember {
seenInvitesStore.seenSpaceIds().map { it.toPersistentSet() } seenInvitesStore.seenRoomIds().map { it.toPersistentSet() }
}.collectAsState(persistentSetOf()) }.collectAsState(persistentSetOf())
fun handleEvents(event: HomeSpacesEvents) { fun handleEvents(event: HomeSpacesEvents) {

View file

@ -7,6 +7,7 @@
package io.element.android.features.home.impl.spaces 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.core.SpaceId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
@ -14,7 +15,7 @@ import kotlinx.collections.immutable.ImmutableSet
data class HomeSpacesState( data class HomeSpacesState(
val space: CurrentSpace, val space: CurrentSpace,
val spaceRooms: List<SpaceRoom>, val spaceRooms: List<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<SpaceId>, val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean, val hideInvitesAvatar: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit, val eventSink: (HomeSpacesEvents) -> Unit,
) )

View file

@ -8,7 +8,7 @@
package io.element.android.features.home.impl.spaces package io.element.android.features.home.impl.spaces
import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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 io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
@ -18,12 +18,12 @@ open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
aHomeSpacesState( aHomeSpacesState(
spaceRooms = SpaceRoomProvider().values.toList(), spaceRooms = SpaceRoomProvider().values.toList(),
seenSpaceInvites = setOf( seenSpaceInvites = setOf(
SpaceId("!spaceId3:example.com"), RoomId("!spaceId3:example.com"),
), ),
), ),
aHomeSpacesState( aHomeSpacesState(
space = CurrentSpace.Space( space = CurrentSpace.Space(
spaceRoom = aSpaceRooms(spaceId = SpaceId("!mySpace:example.com")) spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com"))
), ),
spaceRooms = aListOfSpaceRooms(), spaceRooms = aListOfSpaceRooms(),
), ),
@ -33,7 +33,7 @@ open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
internal fun aHomeSpacesState( internal fun aHomeSpacesState(
space: CurrentSpace = CurrentSpace.Root, space: CurrentSpace = CurrentSpace.Root,
spaceRooms: List<SpaceRoom> = aListOfSpaceRooms(), spaceRooms: List<SpaceRoom> = aListOfSpaceRooms(),
seenSpaceInvites: Set<SpaceId> = emptySet(), seenSpaceInvites: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false, hideInvitesAvatar: Boolean = false,
eventSink: (HomeSpacesEvents) -> Unit = {}, eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState( ) = HomeSpacesState(
@ -46,8 +46,8 @@ internal fun aHomeSpacesState(
fun aListOfSpaceRooms(): List<SpaceRoom> { fun aListOfSpaceRooms(): List<SpaceRoom> {
return listOf( return listOf(
aSpaceRooms(spaceId = SpaceId("!spaceId0:example.com")), aSpaceRoom(roomId = RoomId("!spaceId0:example.com")),
aSpaceRooms(spaceId = SpaceId("!spaceId1:example.com")), aSpaceRoom(roomId = RoomId("!spaceId1:example.com")),
aSpaceRooms(spaceId = SpaceId("!spaceId2:example.com")), aSpaceRoom(roomId = RoomId("!spaceId2:example.com")),
) )
} }

View file

@ -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.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.core.SpaceId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
@ -24,7 +25,7 @@ import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun HomeSpacesView( fun HomeSpacesView(
state: HomeSpacesState, state: HomeSpacesState,
onSpaceClick: (SpaceId) -> Unit, onSpaceClick: (RoomId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn(modifier) { LazyColumn(modifier) {
@ -52,14 +53,14 @@ fun HomeSpacesView(
} }
} }
state.spaceRooms.forEach { state.spaceRooms.forEach {
item(it.spaceId) { item(it.roomId) {
val isInvitation = it.state == CurrentUserMembership.INVITED val isInvitation = it.state == CurrentUserMembership.INVITED
HomeSpaceItemView( HomeSpaceItemView(
spaceRoom = it, spaceRoom = it,
showUnreadIndicator = isInvitation && it.spaceId !in state.seenSpaceInvites, showUnreadIndicator = isInvitation && it.roomId !in state.seenSpaceInvites,
hideAvatars = isInvitation && state.hideInvitesAvatar, hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = { onClick = {
onSpaceClick(it.spaceId) onSpaceClick(it.roomId)
} }
) )
} }

View file

@ -9,7 +9,7 @@ package io.element.android.features.home.impl.spaces
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomAlias 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.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomType 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.room.join.JoinRule
@ -18,42 +18,42 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
class SpaceRoomProvider : PreviewParameterProvider<SpaceRoom> { class SpaceRoomProvider : PreviewParameterProvider<SpaceRoom> {
override val values: Sequence<SpaceRoom> = sequenceOf( override val values: Sequence<SpaceRoom> = sequenceOf(
aSpaceRooms(), aSpaceRoom(),
aSpaceRooms( aSpaceRoom(
numJoinedMembers = 5, numJoinedMembers = 5,
childrenCount = 10, childrenCount = 10,
worldReadable = true, worldReadable = true,
spaceId = SpaceId("!spaceId0:example.com"), roomId = RoomId("!spaceId0:example.com"),
), ),
aSpaceRooms( aSpaceRoom(
numJoinedMembers = 5, numJoinedMembers = 5,
childrenCount = 10, childrenCount = 10,
worldReadable = true, worldReadable = true,
avatarUrl = "anUrl", avatarUrl = "anUrl",
spaceId = SpaceId("!spaceId1:example.com"), roomId = RoomId("!spaceId1:example.com"),
), ),
aSpaceRooms( aSpaceRoom(
name = null, name = null,
numJoinedMembers = 5, numJoinedMembers = 5,
childrenCount = 10, childrenCount = 10,
worldReadable = true, worldReadable = true,
avatarUrl = "anUrl", avatarUrl = "anUrl",
spaceId = SpaceId("!spaceId2:example.com"), roomId = RoomId("!spaceId2:example.com"),
state = CurrentUserMembership.INVITED, state = CurrentUserMembership.INVITED,
), ),
aSpaceRooms( aSpaceRoom(
name = null, name = null,
numJoinedMembers = 5, numJoinedMembers = 5,
childrenCount = 10, childrenCount = 10,
worldReadable = true, worldReadable = true,
avatarUrl = "anUrl", avatarUrl = "anUrl",
spaceId = SpaceId("!spaceId3:example.com"), roomId = RoomId("!spaceId3:example.com"),
state = CurrentUserMembership.INVITED, state = CurrentUserMembership.INVITED,
), ),
) )
} }
fun aSpaceRooms( fun aSpaceRoom(
name: String? = "Space name", name: String? = "Space name",
avatarUrl: String? = null, avatarUrl: String? = null,
canonicalAlias: RoomAlias? = null, canonicalAlias: RoomAlias? = null,
@ -62,7 +62,7 @@ fun aSpaceRooms(
heroes: List<MatrixUser> = emptyList(), heroes: List<MatrixUser> = emptyList(),
joinRule: JoinRule? = null, joinRule: JoinRule? = null,
numJoinedMembers: Int = 0, numJoinedMembers: Int = 0,
spaceId: SpaceId = SpaceId("!spaceId:example.com"), roomId: RoomId = RoomId("!roomId:example.com"),
roomType: RoomType = RoomType.Space, roomType: RoomType = RoomType.Space,
state: CurrentUserMembership? = null, state: CurrentUserMembership? = null,
topic: String? = null, topic: String? = null,
@ -76,7 +76,7 @@ fun aSpaceRooms(
heroes = heroes, heroes = heroes,
joinRule = joinRule, joinRule = joinRule,
numJoinedMembers = numJoinedMembers, numJoinedMembers = numJoinedMembers,
spaceId = spaceId, roomId = roomId,
roomType = roomType, roomType = roomType,
state = state, state = state,
topic = topic, topic = topic,

View file

@ -38,9 +38,3 @@ interface SeenInvitesStore {
*/ */
suspend fun clear() suspend fun clear()
} }
fun SeenInvitesStore.seenSpaceIds(): Flow<Set<SpaceId>> {
return seenRoomIds().map { roomIds ->
roomIds.map { it.toSpaceId() }.toSet()
}
}

View file

@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.api.spaces 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.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.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomType 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.room.join.JoinRule
@ -23,7 +23,7 @@ data class SpaceRoom(
val heroes: List<MatrixUser>, val heroes: List<MatrixUser>,
val joinRule: JoinRule?, val joinRule: JoinRule?,
val numJoinedMembers: Int, val numJoinedMembers: Int,
val spaceId: SpaceId, val roomId: RoomId,
val roomType: RoomType, val roomType: RoomType,
val state: CurrentUserMembership?, val state: CurrentUserMembership?,
val topic: String?, val topic: String?,

View file

@ -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<List<SpaceRoom>>
val paginationStatusFlow: StateFlow<PaginationStatus>
suspend fun paginate(): Result<Unit>
}

View file

@ -7,9 +7,12 @@
package io.element.android.libraries.matrix.api.spaces package io.element.android.libraries.matrix.api.spaces
import io.element.android.libraries.matrix.api.core.SpaceId
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
interface SpaceService { interface SpaceService {
val spaceRooms: SharedFlow<List<SpaceRoom>> val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
suspend fun joinedSpaces(): Result<List<SpaceRoom>> suspend fun joinedSpaces(): Result<List<SpaceRoom>>
suspend fun spaceRoomList(spaceId: SpaceId): SpaceRoomList
} }

View file

@ -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<List<SpaceRoom>>(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<Unit> {
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
}
}
}

View file

@ -8,7 +8,9 @@
package io.element.android.libraries.matrix.impl.spaces package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.core.extensions.runCatchingExceptions 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.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.api.spaces.SpaceService
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@ -21,11 +23,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.SpaceListUpdate import org.matrix.rustcomponents.sdk.SpaceListUpdate
import org.matrix.rustcomponents.sdk.SpaceServiceInterface import org.matrix.rustcomponents.sdk.SpaceServiceInterface
@ -38,104 +37,49 @@ class RustSpaceService(
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher, private val sessionDispatcher: CoroutineDispatcher,
) : SpaceService { ) : SpaceService {
private val mapper = SpaceRoomMapper() private val spaceRoomMapper = SpaceRoomMapper()
private val mutex = Mutex() override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(spaceRoomsFlow, spaceRoomMapper)
override val spaceRooms = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) { override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions { runCatchingExceptions {
innerSpaceService.joinedSpaces() innerSpaceService.joinedSpaces()
.map { .map {
it.let(mapper::map) it.let(spaceRoomMapper::map)
} }
} }
} }
// override suspend fun spaceRoomList(spaceId: SpaceId): Result<List<SpaceRoom>> = withContext(sessionDispatcher) { override suspend fun spaceRoomList(spaceId: SpaceId): SpaceRoomList {
// runCatchingExceptions { val innerSpaceRoomList = innerSpaceService.spaceRoomList(spaceId.value)
// innerSpaceService.spaceRoomList(spaceId.value) return RustSpaceRoomList(
// } inner = innerSpaceRoomList,
// } sessionCoroutineScope = sessionCoroutineScope,
spaceRoomMapper = spaceRoomMapper
)
}
init { init {
innerSpaceService innerSpaceService
.spaceDiffFlow() .spaceListUpdate()
.onEach { .onEach { updates ->
handeUpdate(it) spaceListUpdateProcessor.postUpdates(updates)
} }
.launchIn(sessionCoroutineScope) .launchIn(sessionCoroutineScope)
} }
private suspend fun handeUpdate(spaceListUpdates: List<SpaceListUpdate>) {
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<SpaceRoom>.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<List<SpaceListUpdate>> = internal fun SpaceServiceInterface.spaceListUpdate(): Flow<List<SpaceListUpdate>> =
callbackFlow { callbackFlow {
val listener = object : SpaceServiceJoinedSpacesListener { val listener = object : SpaceServiceJoinedSpacesListener {
override fun onUpdate(roomUpdates: List<SpaceListUpdate>) { override fun onUpdate(roomUpdates: List<SpaceListUpdate>) {
trySendBlocking(roomUpdates) trySendBlocking(roomUpdates)
} }
} }
Timber.d("Open spaceDiffFlow for SpaceServiceInterface ${this@spaceDiffFlow}") Timber.d("Open spaceDiffFlow for SpaceServiceInterface ${this@spaceListUpdate}")
val taskHandle = subscribeToJoinedSpaces(listener) val taskHandle = subscribeToJoinedSpaces(listener)
awaitClose { awaitClose {
Timber.d("Close spaceDiffFlow for SpaceServiceInterface ${this@spaceDiffFlow}") Timber.d("Close spaceDiffFlow for SpaceServiceInterface ${this@spaceListUpdate}")
taskHandle.cancelAndDestroy() taskHandle.cancelAndDestroy()
} }
}.catch { }.catch {

View file

@ -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<List<SpaceRoom>>,
private val mapper: SpaceRoomMapper,
) {
private val mutex = Mutex()
suspend fun postUpdates(updates: List<SpaceListUpdate>) {
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<SpaceRoom>.() -> Unit) =
mutex.withLock {
val spaceRooms = if (spaceRoomsFlow.replayCache.isNotEmpty()) {
spaceRoomsFlow.first().toMutableList()
} else {
mutableListOf()
}
block(spaceRooms)
spaceRoomsFlow.tryEmit(spaceRooms)
}
private fun MutableList<SpaceRoom>.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()
}
}
}
}

View file

@ -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<SpaceRoomListPaginationState> = 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<List<SpaceListUpdate>> =
callbackFlow {
val listener = object : SpaceRoomListEntriesListener {
override fun onUpdate(rooms: List<SpaceListUpdate>) {
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)

View file

@ -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.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.RoomAlias 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.core.SpaceId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.join.map
@ -26,7 +27,7 @@ class SpaceRoomMapper {
joinRule = spaceRoom.joinRule?.map(), joinRule = spaceRoom.joinRule?.map(),
name = spaceRoom.name, name = spaceRoom.name,
numJoinedMembers = spaceRoom.numJoinedMembers.toInt(), numJoinedMembers = spaceRoom.numJoinedMembers.toInt(),
spaceId = spaceRoom.roomId.let(::SpaceId), roomId = RoomId(spaceRoom.roomId),
roomType = spaceRoom.roomType.map(), roomType = spaceRoom.roomType.map(),
state = spaceRoom.state?.map(), state = spaceRoom.state?.map(),
topic = spaceRoom.topic, topic = spaceRoom.topic,

View file

@ -12,7 +12,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
fun SpaceRoom.getAvatarData(size: AvatarSize) = AvatarData( fun SpaceRoom.getAvatarData(size: AvatarSize) = AvatarData(
id = spaceId.value, id = roomId.value,
name = name, name = name,
url = avatarUrl, url = avatarUrl,
size = size, size = size,