Merge pull request #1865 from vector-im/feature/fga/room_list_dynamic_api

RoomList : rework a bit the api and make usage of entriesWithDynamicAdapter
This commit is contained in:
ganfra 2023-11-23 12:07:55 +01:00 committed by GitHub
commit 6e4635b4cd
17 changed files with 317 additions and 213 deletions

View file

@ -57,7 +57,7 @@ class InviteListPresenter @Inject constructor(
override fun present(): InviteListState { override fun present(): InviteListState {
val invites by client val invites by client
.roomListService .roomListService
.invites() .invites
.summaries .summaries
.collectAsState() .collectAsState()

View file

@ -65,7 +65,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) } var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = remember { mutableStateOf(Async.Uninitialized) } val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = remember { mutableStateOf(Async.Uninitialized) }
val summaries by client.roomListService.allRooms().summaries.collectAsState() val summaries by client.roomListService.allRooms.summaries.collectAsState()
LaunchedEffect(query, summaries) { LaunchedEffect(query, summaries) {
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>() val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()

View file

@ -105,7 +105,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
} }
private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<RoomSummary.Filled>>) { private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<RoomSummary.Filled>>) {
roomListService.allRooms() roomListService.allRooms
.summaries .summaries
.onEach { .onEach {
updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode) updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode)

View file

@ -45,7 +45,7 @@ class DefaultInviteStateDataSource @Inject constructor(
override fun inviteState(): InvitesState { override fun inviteState(): InvitesState {
val invites by client val invites by client
.roomListService .roomListService
.invites() .invites
.summaries .summaries
.collectAsState() .collectAsState()

View file

@ -71,7 +71,7 @@ class RoomListDataSource @Inject constructor(
fun launchIn(coroutineScope: CoroutineScope) { fun launchIn(coroutineScope: CoroutineScope) {
roomListService roomListService
.allRooms() .allRooms
.summaries .summaries
.onEach { roomSummaries -> .onEach { roomSummaries ->
replaceWith(roomSummaries) replaceWith(roomSummaries)
@ -106,7 +106,7 @@ class RoomListDataSource @Inject constructor(
notificationSettingsService.notificationSettingsChangeFlow notificationSettingsService.notificationSettingsChangeFlow
.debounce(0.5.seconds) .debounce(0.5.seconds)
.onEach { .onEach {
roomListService.rebuildRoomSummaries() roomListService.allRooms.rebuildSummaries()
} }
.launchIn(appScope) .launchIn(appScope)
} }

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.roomlist
/**
* RoomList with dynamic filtering and loading.
* This is useful for large lists of rooms.
* It lets load rooms on demand and filter them.
*/
interface DynamicRoomList : RoomList {
companion object {
const val DEFAULT_PAGE_SIZE = 20
const val DEFAULT_PAGES_TO_LOAD = 10
}
sealed interface Filter {
/**
* No filter applied.
*/
data object All : Filter
/**
* Filter all rooms.
*/
data object None : Filter
/**
* Filter rooms by normalized room name.
*/
data class NormalizedMatchRoomName(val pattern: String) : Filter
}
/**
* Load more rooms into the list if possible.
*/
suspend fun loadMore()
/**
* Reset the list to its initial size.
*/
suspend fun reset()
/**
* Update the filter to apply to the list.
* @param filter the filter to apply.
*/
suspend fun updateFilter(filter: Filter)
}

View file

@ -28,6 +28,7 @@ import kotlin.time.Duration
* Can be retrieved from [RoomListService] methods. * Can be retrieved from [RoomListService] methods.
*/ */
interface RoomList { interface RoomList {
sealed interface LoadingState { sealed interface LoadingState {
data object NotLoaded : LoadingState data object NotLoaded : LoadingState
data class Loaded(val numberOfRooms: Int) : LoadingState data class Loaded(val numberOfRooms: Int) : LoadingState
@ -43,6 +44,12 @@ interface RoomList {
* This is useful to know if a specific set of rooms is loaded or not. * This is useful to know if a specific set of rooms is loaded or not.
*/ */
val loadingState: StateFlow<LoadingState> val loadingState: StateFlow<LoadingState>
/**
* Force a refresh of the room summaries.
* Might be useful for some situations where we are not notified of changes.
*/
suspend fun rebuildSummaries()
} }
suspend fun RoomList.awaitLoaded(timeout: Duration = Duration.INFINITE) { suspend fun RoomList.awaitLoaded(timeout: Duration = Duration.INFINITE) {

View file

@ -41,12 +41,12 @@ interface RoomListService {
* returns a [RoomList] object of all rooms we want to display. * returns a [RoomList] object of all rooms we want to display.
* This will exclude some rooms like the invites, or spaces. * This will exclude some rooms like the invites, or spaces.
*/ */
fun allRooms(): RoomList val allRooms: RoomList
/** /**
* returns a [RoomList] object of all invites. * returns a [RoomList] object of all invites.
*/ */
fun invites(): RoomList val invites: RoomList
/** /**
* Will set the visible range of all rooms. * Will set the visible range of all rooms.
@ -54,11 +54,6 @@ interface RoomListService {
*/ */
fun updateAllRoomsVisibleRange(range: IntRange) fun updateAllRoomsVisibleRange(range: IntRange)
/**
* Rebuild the room summaries, required when we know some data may have changed. (E.g. room notification settings)
*/
fun rebuildRoomSummaries()
/** /**
* The sync indicator as a flow. * The sync indicator as a flow.
*/ */

View file

@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.sync.RustSyncService
@ -171,7 +172,11 @@ class RustMatrixClient constructor(
RustRoomListService( RustRoomListService(
innerRoomListService = innerRoomListService, innerRoomListService = innerRoomListService,
sessionCoroutineScope = sessionCoroutineScope, sessionCoroutineScope = sessionCoroutineScope,
dispatcher = sessionDispatcher, roomListFactory = RoomListFactory(
innerRoomListService = innerRoomListService,
coroutineScope = sessionCoroutineScope,
dispatcher = sessionDispatcher,
),
) )
override val roomListService: RoomListService override val roomListService: RoomListService
@ -200,7 +205,7 @@ class RustMatrixClient constructor(
var cachedPairOfRoom = pairOfRoom(roomId) var cachedPairOfRoom = pairOfRoom(roomId)
if (cachedPairOfRoom == null) { if (cachedPairOfRoom == null) {
//... otherwise, lets wait for the SS to load all rooms and check again. //... otherwise, lets wait for the SS to load all rooms and check again.
roomListService.allRooms().awaitLoaded() roomListService.allRooms.awaitLoaded()
cachedPairOfRoom = pairOfRoom(roomId) cachedPairOfRoom = pairOfRoom(roomId)
} }
cachedPairOfRoom?.let { (roomListItem, fullRoom) -> cachedPairOfRoom?.let { (roomListItem, fullRoom) ->
@ -274,7 +279,7 @@ class RustMatrixClient constructor(
// Wait to receive the room back from the sync // Wait to receive the room back from the sync
withTimeout(30_000L) { withTimeout(30_000L) {
roomListService.allRooms().summaries roomListService.allRooms.summaries
.filter { roomSummaries -> .filter { roomSummaries ->
roomSummaries.map { it.identifier() }.contains(roomId.value) roomSummaries.map { it.identifier() }.contains(roomId.value)
} }

View file

@ -16,14 +16,10 @@
package io.element.android.libraries.matrix.impl.roomlist package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomList import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.StateFlow
/** internal sealed interface RoomListDynamicEvents {
* Simple implementation of [RoomList] where state flows are provided through constructor. data object Reset : RoomListDynamicEvents
*/ data object LoadMore : RoomListDynamicEvents
class RustRoomList( data class SetFilter(val filter: RoomListEntriesDynamicFilterKind) : RoomListDynamicEvents
override val summaries: StateFlow<List<RoomSummary>>, }
override val loadingState: StateFlow<RoomList.LoadingState>
) : RoomList

View file

@ -17,15 +17,20 @@
package io.element.android.libraries.matrix.impl.roomlist package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListEntriesListener import org.matrix.rustcomponents.sdk.RoomListEntriesListener
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListInterface import org.matrix.rustcomponents.sdk.RoomListInterface
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListLoadingState
@ -58,25 +63,45 @@ fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
Timber.d(it, "loadingStateFlow() failed") Timber.d(it, "loadingStateFlow() failed")
}.buffer(Channel.UNLIMITED) }.buffer(Channel.UNLIMITED)
fun RoomListInterface.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<List<RoomListEntriesUpdate>> = internal fun RoomListInterface.entriesFlow(
mxCallbackFlow { pageSize: Int,
numberOfPages: Int,
roomListDynamicEvents: Flow<RoomListDynamicEvents>,
initialFilterKind: RoomListEntriesDynamicFilterKind
): Flow<List<RoomListEntriesUpdate>> =
callbackFlow {
val listener = object : RoomListEntriesListener { val listener = object : RoomListEntriesListener {
override fun onUpdate(roomEntriesUpdate: List<RoomListEntriesUpdate>) { override fun onUpdate(roomEntriesUpdate: List<RoomListEntriesUpdate>) {
trySendBlocking(roomEntriesUpdate) trySendBlocking(roomEntriesUpdate)
} }
} }
val result = entries(listener) val result = entriesWithDynamicAdapters(pageSize.toUInt(), listener)
try { val controller = result.controller
onInitialList(result.entries) controller.setFilter(initialFilterKind)
} catch (exception: Exception) { roomListDynamicEvents.onEach { controllerEvents ->
Timber.d("entriesFlow() onInitialList failed.") when (controllerEvents) {
is RoomListDynamicEvents.SetFilter -> {
controller.setFilter(controllerEvents.filter)
}
is RoomListDynamicEvents.LoadMore -> {
repeat(numberOfPages) {
controller.addOnePage()
}
}
is RoomListDynamicEvents.Reset -> {
controller.resetToOnePage()
}
}
}.launchIn(this)
awaitClose {
result.entriesStream.cancelAndDestroy()
result.destroy()
} }
result.entriesStream
}.catch { }.catch {
Timber.d(it, "entriesFlow() failed") Timber.d(it, "entriesFlow() failed")
}.buffer(Channel.UNLIMITED) }.buffer(Channel.UNLIMITED)
fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> = internal fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
mxCallbackFlow { mxCallbackFlow {
val listener = object : RoomListServiceStateListener { val listener = object : RoomListServiceStateListener {
override fun onUpdate(state: RoomListServiceState) { override fun onUpdate(state: RoomListServiceState) {
@ -88,7 +113,7 @@ fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
} }
}.buffer(Channel.UNLIMITED) }.buffer(Channel.UNLIMITED)
fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> = internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
mxCallbackFlow { mxCallbackFlow {
val listener = object : RoomListServiceSyncIndicatorListener { val listener = object : RoomListServiceSyncIndicatorListener {
override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) { override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) {
@ -104,7 +129,7 @@ fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator>
} }
}.buffer(Channel.UNLIMITED) }.buffer(Channel.UNLIMITED)
fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? { internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
return try { return try {
room(roomId) room(roomId)
} catch (exception: Exception) { } catch (exception: Exception) {

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
internal class RoomListFactory(
private val innerRoomListService: InnerRoomListService,
private val coroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) {
/**
* Creates a room list that will load all rooms in a single page.
* It mimics the usage of the old api.
*/
fun createRoomList(
innerProvider: suspend () -> InnerRoomList
): RoomList {
return createRustRoomList(
pageSize = Int.MAX_VALUE,
numberOfPages = 1,
initialFilterKind = RoomListEntriesDynamicFilterKind.All,
innerRoomListProvider = innerProvider
)
}
/**
* Creates a room list that can be used to load more rooms and filter them dynamically.
*/
fun createDynamicRoomList(
pageSize: Int = DynamicRoomList.DEFAULT_PAGE_SIZE,
pagesToLoad: Int = DynamicRoomList.DEFAULT_PAGES_TO_LOAD,
initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.None,
innerProvider: suspend () -> InnerRoomList
): DynamicRoomList {
return createRustRoomList(
pageSize = pageSize,
numberOfPages = pagesToLoad,
initialFilterKind = initialFilter.toRustFilter(),
innerRoomListProvider = innerProvider
)
}
private fun createRustRoomList(
pageSize: Int,
numberOfPages: Int,
initialFilterKind: RoomListEntriesDynamicFilterKind,
innerRoomListProvider: suspend () -> InnerRoomList
): RustDynamicRoomList {
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
val summariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory)
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>()
var innerRoomList: InnerRoomList? = null
coroutineScope.launch(dispatcher) {
innerRoomList = innerRoomListProvider()
innerRoomList?.let { innerRoomList ->
innerRoomList.entriesFlow(
pageSize = pageSize,
numberOfPages = numberOfPages,
initialFilterKind = initialFilterKind,
roomListDynamicEvents = dynamicEvents
).onEach { update ->
processor.postUpdate(update)
}.launchIn(this)
innerRoomList.loadingStateFlow()
.map { it.toLoadingState() }
.onEach {
loadingStateFlow.value = it
}
.launchIn(this)
}
}.invokeOnCompletion {
innerRoomList?.destroy()
}
return RustDynamicRoomList(summariesFlow, loadingStateFlow, dynamicEvents, processor)
}
}
private class RustDynamicRoomList(
override val summaries: MutableStateFlow<List<RoomSummary>>,
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
private val dynamicEvents: MutableSharedFlow<RoomListDynamicEvents>,
private val processor: RoomSummaryListProcessor,
) : DynamicRoomList {
override suspend fun rebuildSummaries() {
processor.rebuildRoomSummaries()
}
override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter())
dynamicEvents.emit(filterEvent)
}
override suspend fun loadMore() {
dynamicEvents.emit(RoomListDynamicEvents.LoadMore)
}
override suspend fun reset() {
dynamicEvents.emit(RoomListDynamicEvents.Reset)
}
}
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
return when (this) {
is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
RoomListLoadingState.NotLoaded -> RoomList.LoadingState.NotLoaded
}
}
private fun DynamicRoomList.Filter.toRustFilter(): RoomListEntriesDynamicFilterKind {
return when (this) {
DynamicRoomList.Filter.All -> RoomListEntriesDynamicFilterKind.All
is DynamicRoomList.Filter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(this.pattern)
DynamicRoomList.Filter.None -> RoomListEntriesDynamicFilterKind.None
}
}

View file

@ -16,9 +16,7 @@
package io.element.android.libraries.matrix.impl.roomlist package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -39,21 +37,9 @@ class RoomSummaryListProcessor(
) { ) {
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>() private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
private val initLatch = CompletableDeferred<Unit>()
private val mutex = Mutex() private val mutex = Mutex()
suspend fun postEntries(entries: List<RoomListEntry>) {
updateRoomSummaries {
Timber.v("Update rooms from postEntries (with ${entries.size} items) on ${Thread.currentThread()}")
val roomSummaries = entries.parallelMap(::buildSummaryForRoomListEntry)
addAll(roomSummaries)
}
initLatch.complete(Unit)
}
suspend fun postUpdate(updates: List<RoomListEntriesUpdate>) { suspend fun postUpdate(updates: List<RoomListEntriesUpdate>) {
// Makes sure to process first entries before update.
initLatch.await()
updateRoomSummaries { updateRoomSummaries {
Timber.v("Update rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}") Timber.v("Update rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
updates.forEach { update -> updates.forEach { update ->
@ -65,7 +51,7 @@ class RoomSummaryListProcessor(
suspend fun rebuildRoomSummaries() { suspend fun rebuildRoomSummaries() {
updateRoomSummaries { updateRoomSummaries {
forEachIndexed { i, summary -> forEachIndexed { i, summary ->
this[i] = when(summary) { this[i] = when (summary) {
is RoomSummary.Empty -> summary is RoomSummary.Empty -> summary
is RoomSummary.Filled -> buildAndCacheRoomSummaryForIdentifier(summary.identifier()) is RoomSummary.Filled -> buildAndCacheRoomSummaryForIdentifier(summary.identifier())
} }

View file

@ -18,79 +18,34 @@ package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListException import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.RoomListInput import org.matrix.rustcomponents.sdk.RoomListInput
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListRange import org.matrix.rustcomponents.sdk.RoomListRange
import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import timber.log.Timber import timber.log.Timber
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
class RustRoomListService( internal class RustRoomListService(
private val innerRoomListService: InnerRustRoomListService, private val innerRoomListService: InnerRustRoomListService,
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher, private val roomListFactory: RoomListFactory,
roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) : RoomListService { ) : RoomListService {
private val allRooms = MutableStateFlow<List<RoomSummary>>(emptyList()) override val allRooms: RoomList = roomListFactory.createRoomList {
private val inviteRooms = MutableStateFlow<List<RoomSummary>>(emptyList()) innerRoomListService.allRooms()
private val allRoomsLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, dispatcher, roomSummaryDetailsFactory)
private val invitesLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, dispatcher, roomSummaryDetailsFactory)
init {
sessionCoroutineScope.launch(dispatcher) {
val allRooms = innerRoomListService.allRooms()
allRooms
.observeEntriesWithProcessor(allRoomsListProcessor)
.launchIn(this)
allRooms
.observeLoadingState(allRoomsLoadingState)
.launchIn(this)
launch {
// Wait until running, as invites is only available after that
innerRoomListService.stateFlow().first {
it == RoomListServiceState.RUNNING
}
val invites = innerRoomListService.invites()
invites
.observeEntriesWithProcessor(inviteRoomsListProcessor)
.launchIn(this)
invites
.observeLoadingState(invitesLoadingState)
.launchIn(this)
}
}
} }
override fun allRooms(): RoomList { override val invites: RoomList = roomListFactory.createRoomList {
return RustRoomList(allRooms, allRoomsLoadingState) innerRoomListService.invites()
}
override fun invites(): RoomList {
return RustRoomList(inviteRooms, invitesLoadingState)
} }
override fun updateAllRoomsVisibleRange(range: IntRange) { override fun updateAllRoomsVisibleRange(range: IntRange) {
@ -107,12 +62,6 @@ class RustRoomListService(
} }
} }
override fun rebuildRoomSummaries() {
sessionCoroutineScope.launch {
allRoomsListProcessor.rebuildRoomSummaries()
}
}
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> = override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
innerRoomListService.syncIndicator() innerRoomListService.syncIndicator()
.map { it.toSyncIndicator() } .map { it.toSyncIndicator() }
@ -132,13 +81,6 @@ class RustRoomListService(
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle) .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle)
} }
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
return when (this) {
is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
RoomListLoadingState.NotLoaded -> RoomList.LoadingState.NotLoaded
}
}
private fun RoomListServiceState.toRoomListState(): RoomListService.State { private fun RoomListServiceState.toRoomListState(): RoomListService.State {
return when (this) { return when (this) {
RoomListServiceState.INITIAL, RoomListServiceState.INITIAL,
@ -156,20 +98,3 @@ private fun RoomListServiceSyncIndicator.toSyncIndicator(): RoomListService.Sync
RoomListServiceSyncIndicator.HIDE -> RoomListService.SyncIndicator.Hide RoomListServiceSyncIndicator.HIDE -> RoomListService.SyncIndicator.Hide
} }
} }
private fun org.matrix.rustcomponents.sdk.RoomList.observeEntriesWithProcessor(processor: RoomSummaryListProcessor): Flow<List<RoomListEntriesUpdate>> {
return entriesFlow { roomListEntries ->
processor.postEntries(roomListEntries)
}.onEach { update ->
processor.postUpdate(update)
}
}
private fun org.matrix.rustcomponents.sdk.RoomList.observeLoadingState(stateFlow: MutableStateFlow<RoomList.LoadingState>): Flow<RoomList.LoadingState> {
return loadingStateFlow()
.map { it.toLoadingState() }
.onEach {
stateFlow.value = it
}
}

View file

@ -22,12 +22,10 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.test.A_ROOM_ID 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.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.RoomList import org.matrix.rustcomponents.sdk.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
@ -38,7 +36,6 @@ import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener
import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.TaskHandle
import kotlin.time.Duration.Companion.milliseconds
// NOTE: this class is using a fake implementation of a Rust SDK interface which returns actual Rust objects with pointers. // NOTE: this class is using a fake implementation of a Rust SDK interface which returns actual Rust objects with pointers.
// Since we don't access the data in those objects, this is fine for our tests, but that's as far as we can test this class. // Since we don't access the data in those objects, this is fine for our tests, but that's as far as we can test this class.
@ -46,38 +43,11 @@ class RoomSummaryListProcessorTests {
private val summaries = MutableStateFlow<List<RoomSummary>>(emptyList()) private val summaries = MutableStateFlow<List<RoomSummary>>(emptyList())
@Test
fun `postUpdates can't start until postEntries is done`() = runTest {
val processor = createProcessor()
val update = listOf(RoomListEntriesUpdate.Reset(emptyList()))
val timeoutError = runCatching {
withTimeout(10.milliseconds) { processor.postUpdate(update) }
}.exceptionOrNull()
assertThat(timeoutError).isInstanceOf(CancellationException::class.java)
processor.postEntries(listOf(RoomListEntry.Empty))
processor.postUpdate(update)
}
@Test
fun `postEntries adds all new entries with no diffing`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
processor.postEntries(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty))
assertThat(summaries.value.count()).isEqualTo(4)
}
@Test @Test
fun `Append adds new entries at the end of the list`() = runTest { fun `Append adds new entries at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor() val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty)))) processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty))))
assertThat(summaries.value.count()).isEqualTo(4) assertThat(summaries.value.count()).isEqualTo(4)
@ -88,10 +58,6 @@ class RoomSummaryListProcessorTests {
fun `PushBack adds a new entry at the end of the list`() = runTest { fun `PushBack adds a new entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor() val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(RoomListEntry.Empty))) processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(2) assertThat(summaries.value.count()).isEqualTo(2)
@ -102,10 +68,6 @@ class RoomSummaryListProcessorTests {
fun `PushFront inserts a new entry at the start of the list`() = runTest { fun `PushFront inserts a new entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor() val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(RoomListEntry.Empty))) processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(2) assertThat(summaries.value.count()).isEqualTo(2)
@ -118,9 +80,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), RoomListEntry.Empty))) processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(1) assertThat(summaries.value.count()).isEqualTo(1)
@ -133,9 +92,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), RoomListEntry.Empty))) processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(2) assertThat(summaries.value.count()).isEqualTo(2)
@ -148,9 +104,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Remove(index.toUInt()))) processor.postUpdate(listOf(RoomListEntriesUpdate.Remove(index.toUInt())))
assertThat(summaries.value.count()).isEqualTo(1) assertThat(summaries.value.count()).isEqualTo(1)
@ -163,9 +116,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PopBack)) processor.postUpdate(listOf(RoomListEntriesUpdate.PopBack))
assertThat(summaries.value.count()).isEqualTo(1) assertThat(summaries.value.count()).isEqualTo(1)
@ -178,9 +128,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PopFront)) processor.postUpdate(listOf(RoomListEntriesUpdate.PopFront))
assertThat(summaries.value.count()).isEqualTo(1) assertThat(summaries.value.count()).isEqualTo(1)
@ -192,9 +139,6 @@ class RoomSummaryListProcessorTests {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
val processor = createProcessor() val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Clear)) processor.postUpdate(listOf(RoomListEntriesUpdate.Clear))
assertThat(summaries.value).isEmpty() assertThat(summaries.value).isEmpty()
@ -206,9 +150,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Truncate(1u))) processor.postUpdate(listOf(RoomListEntriesUpdate.Truncate(1u)))
assertThat(summaries.value.count()).isEqualTo(1) assertThat(summaries.value.count()).isEqualTo(1)

View file

@ -54,28 +54,20 @@ class FakeRoomListService : RoomListService {
var latestSlidingSyncRange: IntRange? = null var latestSlidingSyncRange: IntRange? = null
private set private set
override val allRooms: RoomList = SimplePagedRoomList(
allRoomSummariesFlow,
allRoomsLoadingStateFlow,
)
override val invites: RoomList = SimplePagedRoomList(
inviteRoomSummariesFlow,
inviteRoomsLoadingStateFlow,
)
override fun updateAllRoomsVisibleRange(range: IntRange) { override fun updateAllRoomsVisibleRange(range: IntRange) {
latestSlidingSyncRange = range latestSlidingSyncRange = range
} }
override fun rebuildRoomSummaries() {
}
override fun allRooms(): RoomList {
return SimpleRoomList(
summaries = allRoomSummariesFlow,
loadingState = allRoomsLoadingStateFlow
)
}
override fun invites(): RoomList {
return SimpleRoomList(
summaries = inviteRoomSummariesFlow,
loadingState = inviteRoomsLoadingStateFlow
)
}
override val state: StateFlow<RoomListService.State> = roomListStateFlow override val state: StateFlow<RoomListService.State> = roomListStateFlow
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> = syncIndicatorStateFlow override val syncIndicator: StateFlow<RoomListService.SyncIndicator> = syncIndicatorStateFlow

View file

@ -16,11 +16,29 @@
package io.element.android.libraries.matrix.test.roomlist package io.element.android.libraries.matrix.test.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
data class SimpleRoomList( data class SimplePagedRoomList(
override val summaries: StateFlow<List<RoomSummary>>, override val summaries: StateFlow<List<RoomSummary>>,
override val loadingState: StateFlow<RoomList.LoadingState> override val loadingState: StateFlow<RoomList.LoadingState>
) : RoomList ) : DynamicRoomList {
override suspend fun loadMore() {
//No-op
}
override suspend fun reset() {
//No-op
}
override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
//No-op
}
override suspend fun rebuildSummaries() {
//No-op
}
}