sdk (space) : use the new space/spaceUpdate api from space list

This commit is contained in:
ganfra 2025-09-24 12:48:24 +02:00
parent 4a093bea67
commit 718b4dd11b
12 changed files with 38 additions and 128 deletions

View file

@ -94,9 +94,7 @@ class JoinRoomPresenter(
val roomInfo by remember { val roomInfo by remember {
matrixClient.getRoomInfoFlow(roomId) matrixClient.getRoomInfoFlow(roomId)
}.collectAsState(initial = Optional.empty()) }.collectAsState(initial = Optional.empty())
val spaceRoom by remember { val spaceRoom by spaceList.currentSpaceFlow.collectAsState()
spaceList.currentSpaceFlow()
}.collectAsState()
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }

View file

@ -29,7 +29,6 @@ import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Optional
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@Inject @Inject
@ -66,7 +65,7 @@ class SpacePresenter(
} }
}.collectAsState() }.collectAsState()
val currentSpace by remember { spaceRoomList.currentSpaceFlow() }.collectAsState(Optional.empty()) val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
fun handleEvents(event: SpaceEvents) { fun handleEvents(event: SpaceEvents) {
when (event) { when (event) {

View file

@ -17,7 +17,7 @@ interface SpaceRoomList {
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
} }
fun currentSpaceFlow(): StateFlow<Optional<SpaceRoom>> val currentSpaceFlow: StateFlow<Optional<SpaceRoom>>
val spaceRoomsFlow: Flow<List<SpaceRoom>> val spaceRoomsFlow: Flow<List<SpaceRoom>>
val paginationStatusFlow: StateFlow<PaginationStatus> val paginationStatusFlow: StateFlow<PaginationStatus>

View file

@ -8,15 +8,14 @@
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.RoomId
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.SpaceRoomList
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
@ -24,25 +23,21 @@ import java.util.Optional
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomList( class RustSpaceRoomList(
private val roomId: RoomId,
private val innerProvider: suspend () -> InnerSpaceRoomList, private val innerProvider: suspend () -> InnerSpaceRoomList,
sessionCoroutineScope: CoroutineScope, sessionCoroutineScope: CoroutineScope,
spaceRoomMapper: SpaceRoomMapper, spaceRoomMapper: SpaceRoomMapper,
private val spaceRoomCache: SpaceRoomCache,
) : SpaceRoomList { ) : SpaceRoomList {
private val inner = CompletableDeferred<InnerSpaceRoomList>() private val inner = CompletableDeferred<InnerSpaceRoomList>()
override fun currentSpaceFlow(): StateFlow<Optional<SpaceRoom>> { override val currentSpaceFlow = MutableStateFlow<Optional<SpaceRoom>>(Optional.empty())
return spaceRoomCache.getSpaceRoomFlow(roomId)
}
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = Int.MAX_VALUE) override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
override val paginationStatusFlow: MutableStateFlow<SpaceRoomList.PaginationStatus> = override val paginationStatusFlow: MutableStateFlow<SpaceRoomList.PaginationStatus> =
MutableStateFlow(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false)) MutableStateFlow(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
private val spaceListUpdateProcessor = SpaceListUpdateProcessor( private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow, spaceRoomsFlow = spaceRoomsFlow,
mapper = spaceRoomMapper, mapper = spaceRoomMapper
spaceRoomCache = spaceRoomCache
) )
init { init {
@ -64,6 +59,14 @@ class RustSpaceRoomList(
} }
.collect() .collect()
} }
sessionCoroutineScope.launch {
inner.await().spaceUpdateFlow()
.map { space -> space.map(spaceRoomMapper::map) }
.onEach { space ->
currentSpaceFlow.emit(space)
}
.collect()
}
} }
override suspend fun paginate(): Result<Unit> { override suspend fun paginate(): Result<Unit> {

View file

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

View file

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

View file

@ -1,36 +0,0 @@
/*
* 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.coroutine.mapState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import java.util.Optional
/**
* 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<Map<RoomId, SpaceRoom?>>(emptyMap())
fun getSpaceRoomFlow(roomId: RoomId): StateFlow<Optional<SpaceRoom>> {
return inMemoryCache.mapState { Optional.ofNullable(it[roomId]) }
}
fun update(spaceRooms: List<SpaceRoom>) {
inMemoryCache.update { currentValues ->
val newValues = spaceRooms
.filter { it.isSpace }
.associateBy { it.roomId }
currentValues + newValues
}
}
}

View file

@ -16,11 +16,14 @@ 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 org.matrix.rustcomponents.sdk.SpaceListUpdate import org.matrix.rustcomponents.sdk.SpaceListUpdate
import org.matrix.rustcomponents.sdk.SpaceRoom
import org.matrix.rustcomponents.sdk.SpaceRoomListEntriesListener import org.matrix.rustcomponents.sdk.SpaceRoomListEntriesListener
import org.matrix.rustcomponents.sdk.SpaceRoomListInterface import org.matrix.rustcomponents.sdk.SpaceRoomListInterface
import org.matrix.rustcomponents.sdk.SpaceRoomListPaginationStateListener import org.matrix.rustcomponents.sdk.SpaceRoomListPaginationStateListener
import org.matrix.rustcomponents.sdk.SpaceRoomListSpaceListener
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import java.util.Optional
internal fun SpaceRoomListInterface.paginationStateFlow(): Flow<SpaceRoomListPaginationState> = callbackFlow { internal fun SpaceRoomListInterface.paginationStateFlow(): Flow<SpaceRoomListPaginationState> = callbackFlow {
val listener = object : SpaceRoomListPaginationStateListener { val listener = object : SpaceRoomListPaginationStateListener {
@ -55,3 +58,21 @@ internal fun SpaceRoomListInterface.spaceListUpdateFlow(): Flow<List<SpaceListUp
}.catch { }.catch {
Timber.d(it, "spaceListUpdateFlow() failed") Timber.d(it, "spaceListUpdateFlow() failed")
}.buffer(Channel.UNLIMITED) }.buffer(Channel.UNLIMITED)
internal fun SpaceRoomListInterface.spaceUpdateFlow(): Flow<Optional<SpaceRoom>> =
callbackFlow {
val listener = object : SpaceRoomListSpaceListener {
override fun onUpdate(space: SpaceRoom?) {
trySendBlocking(Optional.ofNullable(space))
}
}
Timber.d("Open spaceUpdateFlow for SpaceRoomListInterface ${this@spaceUpdateFlow}")
trySendBlocking(Optional.ofNullable(space()))
val taskHandle = subscribeToSpaceUpdates(listener)
awaitClose {
Timber.d("Close spaceUpdateFlow for SpaceRoomListInterface ${this@spaceUpdateFlow}")
taskHandle.cancelAndDestroy()
}
}.catch {
Timber.d(it, "spaceUpdateFlow() failed")
}.buffer(Channel.UNLIMITED)

View file

@ -185,6 +185,5 @@ class RoomSummaryListProcessorTest {
) = SpaceListUpdateProcessor( ) = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow, spaceRoomsFlow = spaceRoomsFlow,
mapper = SpaceRoomMapper(), mapper = SpaceRoomMapper(),
spaceRoomCache = SpaceRoomCache(),
) )
} }

View file

@ -11,13 +11,10 @@ package io.element.android.libraries.matrix.impl.spaces
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoom import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoom
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSpaceRoomList import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSpaceRoomList
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.previewutils.room.aSpaceRoom
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -26,7 +23,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.SpaceListUpdate import org.matrix.rustcomponents.sdk.SpaceListUpdate
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import kotlin.jvm.optionals.getOrNull
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomListTest { class RustSpaceRoomListTest {
@ -87,33 +83,15 @@ class RustSpaceRoomListTest {
paginateResult.assertions().isCalledOnce() paginateResult.assertions().isCalledOnce()
} }
@Test
fun `currentSpaceFlow reads value from the SpaceRoomCache`() = runTest {
val spaceRoomCache = SpaceRoomCache()
val sut = createRustSpaceRoomList(
spaceRoomCache = spaceRoomCache,
)
sut.currentSpaceFlow().test {
assertThat(awaitItem().getOrNull()).isNull()
val spaceRoom = aSpaceRoom(roomId = A_ROOM_ID)
spaceRoomCache.update(listOf(spaceRoom))
assertThat(awaitItem().getOrNull()).isEqualTo(spaceRoom)
}
}
private fun TestScope.createRustSpaceRoomList( private fun TestScope.createRustSpaceRoomList(
roomId: RoomId = A_ROOM_ID,
innerSpaceRoomList: InnerSpaceRoomList = FakeFfiSpaceRoomList(), innerSpaceRoomList: InnerSpaceRoomList = FakeFfiSpaceRoomList(),
innerProvider: suspend () -> InnerSpaceRoomList = { innerSpaceRoomList }, innerProvider: suspend () -> InnerSpaceRoomList = { innerSpaceRoomList },
spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(), spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(),
spaceRoomCache: SpaceRoomCache = SpaceRoomCache(),
): RustSpaceRoomList { ): RustSpaceRoomList {
return RustSpaceRoomList( return RustSpaceRoomList(
roomId = roomId,
innerProvider = innerProvider, innerProvider = innerProvider,
sessionCoroutineScope = backgroundScope, sessionCoroutineScope = backgroundScope,
spaceRoomMapper = spaceRoomMapper, spaceRoomMapper = spaceRoomMapper,
spaceRoomCache = spaceRoomCache,
) )
} }
} }

View file

@ -1,46 +0,0 @@
/*
* 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 app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.coroutines.test.runTest
import org.junit.Test
class SpaceRoomCacheTest {
@Test
fun `getSpaceRoomFlow emits items`() = runTest {
val sut = SpaceRoomCache()
sut.getSpaceRoomFlow(A_ROOM_ID).test {
assertThat(awaitItem().isEmpty).isTrue()
val room = aSpaceRoom(
roomId = A_ROOM_ID,
roomType = RoomType.Room,
)
sut.update(listOf(room))
// Not a space, should not be cached
expectNoEvents()
val space = aSpaceRoom(
roomId = A_ROOM_ID,
roomType = RoomType.Space,
)
sut.update(listOf(space))
assertThat(awaitItem().get()).isEqualTo(space)
val spaceOther = aSpaceRoom(
roomId = A_ROOM_ID_2,
roomType = RoomType.Space,
)
sut.update(listOf(spaceOther))
expectNoEvents()
}
}
}

View file

@ -24,7 +24,7 @@ class FakeSpaceRoomList(
private val paginateResult: () -> Result<Unit> = { lambdaError() }, private val paginateResult: () -> Result<Unit> = { lambdaError() },
) : SpaceRoomList { ) : SpaceRoomList {
private val currentSpaceMutableStateFlow: MutableStateFlow<Optional<SpaceRoom>> = MutableStateFlow(Optional.ofNullable(initialSpaceFlowValue)) private val currentSpaceMutableStateFlow: MutableStateFlow<Optional<SpaceRoom>> = MutableStateFlow(Optional.ofNullable(initialSpaceFlowValue))
override fun currentSpaceFlow(): StateFlow<Optional<SpaceRoom>> = currentSpaceMutableStateFlow.asStateFlow() override val currentSpaceFlow: StateFlow<Optional<SpaceRoom>> = currentSpaceMutableStateFlow.asStateFlow()
fun emitCurrentSpace(value: SpaceRoom?) { fun emitCurrentSpace(value: SpaceRoom?) {
currentSpaceMutableStateFlow.value = Optional.ofNullable(value) currentSpaceMutableStateFlow.value = Optional.ofNullable(value)