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:
@@ -57,7 +57,7 @@ class InviteListPresenter @Inject constructor(
|
||||
override fun present(): InviteListState {
|
||||
val invites by client
|
||||
.roomListService
|
||||
.invites()
|
||||
.invites
|
||||
.summaries
|
||||
.collectAsState()
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
|
||||
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
|
||||
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) {
|
||||
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
|
||||
|
||||
@@ -105,7 +105,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<RoomSummary.Filled>>) {
|
||||
roomListService.allRooms()
|
||||
roomListService.allRooms
|
||||
.summaries
|
||||
.onEach {
|
||||
updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode)
|
||||
|
||||
@@ -45,7 +45,7 @@ class DefaultInviteStateDataSource @Inject constructor(
|
||||
override fun inviteState(): InvitesState {
|
||||
val invites by client
|
||||
.roomListService
|
||||
.invites()
|
||||
.invites
|
||||
.summaries
|
||||
.collectAsState()
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class RoomListDataSource @Inject constructor(
|
||||
|
||||
fun launchIn(coroutineScope: CoroutineScope) {
|
||||
roomListService
|
||||
.allRooms()
|
||||
.allRooms
|
||||
.summaries
|
||||
.onEach { roomSummaries ->
|
||||
replaceWith(roomSummaries)
|
||||
@@ -106,7 +106,7 @@ class RoomListDataSource @Inject constructor(
|
||||
notificationSettingsService.notificationSettingsChangeFlow
|
||||
.debounce(0.5.seconds)
|
||||
.onEach {
|
||||
roomListService.rebuildRoomSummaries()
|
||||
roomListService.allRooms.rebuildSummaries()
|
||||
}
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import kotlin.time.Duration
|
||||
* Can be retrieved from [RoomListService] methods.
|
||||
*/
|
||||
interface RoomList {
|
||||
|
||||
sealed interface LoadingState {
|
||||
data object NotLoaded : 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.
|
||||
*/
|
||||
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) {
|
||||
|
||||
@@ -41,12 +41,12 @@ interface RoomListService {
|
||||
* returns a [RoomList] object of all rooms we want to display.
|
||||
* This will exclude some rooms like the invites, or spaces.
|
||||
*/
|
||||
fun allRooms(): RoomList
|
||||
val allRooms: RoomList
|
||||
|
||||
/**
|
||||
* returns a [RoomList] object of all invites.
|
||||
*/
|
||||
fun invites(): RoomList
|
||||
val invites: RoomList
|
||||
|
||||
/**
|
||||
* Will set the visible range of all rooms.
|
||||
@@ -54,11 +54,6 @@ interface RoomListService {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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.RoomSyncSubscriber
|
||||
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.roomOrNull
|
||||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||
@@ -171,7 +172,11 @@ class RustMatrixClient constructor(
|
||||
RustRoomListService(
|
||||
innerRoomListService = innerRoomListService,
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
dispatcher = sessionDispatcher,
|
||||
roomListFactory = RoomListFactory(
|
||||
innerRoomListService = innerRoomListService,
|
||||
coroutineScope = sessionCoroutineScope,
|
||||
dispatcher = sessionDispatcher,
|
||||
),
|
||||
)
|
||||
|
||||
override val roomListService: RoomListService
|
||||
@@ -200,7 +205,7 @@ class RustMatrixClient constructor(
|
||||
var cachedPairOfRoom = pairOfRoom(roomId)
|
||||
if (cachedPairOfRoom == null) {
|
||||
//... otherwise, lets wait for the SS to load all rooms and check again.
|
||||
roomListService.allRooms().awaitLoaded()
|
||||
roomListService.allRooms.awaitLoaded()
|
||||
cachedPairOfRoom = pairOfRoom(roomId)
|
||||
}
|
||||
cachedPairOfRoom?.let { (roomListItem, fullRoom) ->
|
||||
@@ -274,7 +279,7 @@ class RustMatrixClient constructor(
|
||||
|
||||
// Wait to receive the room back from the sync
|
||||
withTimeout(30_000L) {
|
||||
roomListService.allRooms().summaries
|
||||
roomListService.allRooms.summaries
|
||||
.filter { roomSummaries ->
|
||||
roomSummaries.map { it.identifier() }.contains(roomId.value)
|
||||
}
|
||||
|
||||
@@ -16,14 +16,10 @@
|
||||
|
||||
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.RoomSummary
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
||||
|
||||
/**
|
||||
* Simple implementation of [RoomList] where state flows are provided through constructor.
|
||||
*/
|
||||
class RustRoomList(
|
||||
override val summaries: StateFlow<List<RoomSummary>>,
|
||||
override val loadingState: StateFlow<RoomList.LoadingState>
|
||||
) : RoomList
|
||||
internal sealed interface RoomListDynamicEvents {
|
||||
data object Reset : RoomListDynamicEvents
|
||||
data object LoadMore : RoomListDynamicEvents
|
||||
data class SetFilter(val filter: RoomListEntriesDynamicFilterKind) : RoomListDynamicEvents
|
||||
}
|
||||
@@ -17,15 +17,20 @@
|
||||
package io.element.android.libraries.matrix.impl.roomlist
|
||||
|
||||
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 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 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.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntry
|
||||
import org.matrix.rustcomponents.sdk.RoomListInterface
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||
@@ -58,25 +63,45 @@ fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
|
||||
Timber.d(it, "loadingStateFlow() failed")
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
fun RoomListInterface.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<List<RoomListEntriesUpdate>> =
|
||||
mxCallbackFlow {
|
||||
internal fun RoomListInterface.entriesFlow(
|
||||
pageSize: Int,
|
||||
numberOfPages: Int,
|
||||
roomListDynamicEvents: Flow<RoomListDynamicEvents>,
|
||||
initialFilterKind: RoomListEntriesDynamicFilterKind
|
||||
): Flow<List<RoomListEntriesUpdate>> =
|
||||
callbackFlow {
|
||||
val listener = object : RoomListEntriesListener {
|
||||
override fun onUpdate(roomEntriesUpdate: List<RoomListEntriesUpdate>) {
|
||||
trySendBlocking(roomEntriesUpdate)
|
||||
}
|
||||
}
|
||||
val result = entries(listener)
|
||||
try {
|
||||
onInitialList(result.entries)
|
||||
} catch (exception: Exception) {
|
||||
Timber.d("entriesFlow() onInitialList failed.")
|
||||
val result = entriesWithDynamicAdapters(pageSize.toUInt(), listener)
|
||||
val controller = result.controller
|
||||
controller.setFilter(initialFilterKind)
|
||||
roomListDynamicEvents.onEach { controllerEvents ->
|
||||
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 {
|
||||
Timber.d(it, "entriesFlow() failed")
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
|
||||
internal fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : RoomListServiceStateListener {
|
||||
override fun onUpdate(state: RoomListServiceState) {
|
||||
@@ -88,7 +113,7 @@ fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
|
||||
}
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
|
||||
internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : RoomListServiceSyncIndicatorListener {
|
||||
override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) {
|
||||
@@ -104,7 +129,7 @@ fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator>
|
||||
}
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
|
||||
internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
|
||||
return try {
|
||||
room(roomId)
|
||||
} catch (exception: Exception) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
|
||||
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 kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -39,21 +37,9 @@ class RoomSummaryListProcessor(
|
||||
) {
|
||||
|
||||
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
|
||||
private val initLatch = CompletableDeferred<Unit>()
|
||||
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>) {
|
||||
// Makes sure to process first entries before update.
|
||||
initLatch.await()
|
||||
updateRoomSummaries {
|
||||
Timber.v("Update rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
|
||||
updates.forEach { update ->
|
||||
@@ -65,7 +51,7 @@ class RoomSummaryListProcessor(
|
||||
suspend fun rebuildRoomSummaries() {
|
||||
updateRoomSummaries {
|
||||
forEachIndexed { i, summary ->
|
||||
this[i] = when(summary) {
|
||||
this[i] = when (summary) {
|
||||
is RoomSummary.Empty -> summary
|
||||
is RoomSummary.Filled -> buildAndCacheRoomSummaryForIdentifier(summary.identifier())
|
||||
}
|
||||
|
||||
@@ -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.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListException
|
||||
import org.matrix.rustcomponents.sdk.RoomListInput
|
||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||
import org.matrix.rustcomponents.sdk.RoomListRange
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceState
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
|
||||
import timber.log.Timber
|
||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
|
||||
|
||||
class RustRoomListService(
|
||||
internal class RustRoomListService(
|
||||
private val innerRoomListService: InnerRustRoomListService,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
private val roomListFactory: RoomListFactory,
|
||||
) : RoomListService {
|
||||
|
||||
private val allRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
|
||||
private val inviteRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
|
||||
|
||||
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 val allRooms: RoomList = roomListFactory.createRoomList {
|
||||
innerRoomListService.allRooms()
|
||||
}
|
||||
|
||||
override fun allRooms(): RoomList {
|
||||
return RustRoomList(allRooms, allRoomsLoadingState)
|
||||
}
|
||||
|
||||
override fun invites(): RoomList {
|
||||
return RustRoomList(inviteRooms, invitesLoadingState)
|
||||
override val invites: RoomList = roomListFactory.createRoomList {
|
||||
innerRoomListService.invites()
|
||||
}
|
||||
|
||||
override fun updateAllRoomsVisibleRange(range: IntRange) {
|
||||
@@ -107,12 +62,6 @@ class RustRoomListService(
|
||||
}
|
||||
}
|
||||
|
||||
override fun rebuildRoomSummaries() {
|
||||
sessionCoroutineScope.launch {
|
||||
allRoomsListProcessor.rebuildRoomSummaries()
|
||||
}
|
||||
}
|
||||
|
||||
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
|
||||
innerRoomListService.syncIndicator()
|
||||
.map { it.toSyncIndicator() }
|
||||
@@ -132,13 +81,6 @@ class RustRoomListService(
|
||||
.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 {
|
||||
return when (this) {
|
||||
RoomListServiceState.INITIAL,
|
||||
@@ -156,20 +98,3 @@ private fun RoomListServiceSyncIndicator.toSyncIndicator(): RoomListService.Sync
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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_2
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.RoomList
|
||||
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.RoomListServiceSyncIndicatorListener
|
||||
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.
|
||||
// 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())
|
||||
|
||||
@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
|
||||
fun `Append adds new entries at the end of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
val processor = createProcessor()
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty))))
|
||||
|
||||
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 {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
val processor = createProcessor()
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(RoomListEntry.Empty)))
|
||||
|
||||
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 {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
val processor = createProcessor()
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(RoomListEntry.Empty)))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(2)
|
||||
@@ -118,9 +80,6 @@ class RoomSummaryListProcessorTests {
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), RoomListEntry.Empty)))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
@@ -133,9 +92,6 @@ class RoomSummaryListProcessorTests {
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), RoomListEntry.Empty)))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(2)
|
||||
@@ -148,9 +104,6 @@ class RoomSummaryListProcessorTests {
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Remove(index.toUInt())))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
@@ -163,9 +116,6 @@ class RoomSummaryListProcessorTests {
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PopBack))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
@@ -178,9 +128,6 @@ class RoomSummaryListProcessorTests {
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PopFront))
|
||||
|
||||
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))
|
||||
val processor = createProcessor()
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Clear))
|
||||
|
||||
assertThat(summaries.value).isEmpty()
|
||||
@@ -206,9 +150,6 @@ class RoomSummaryListProcessorTests {
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Truncate(1u)))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
|
||||
@@ -54,28 +54,20 @@ class FakeRoomListService : RoomListService {
|
||||
var latestSlidingSyncRange: IntRange? = null
|
||||
private set
|
||||
|
||||
override val allRooms: RoomList = SimplePagedRoomList(
|
||||
allRoomSummariesFlow,
|
||||
allRoomsLoadingStateFlow,
|
||||
)
|
||||
|
||||
override val invites: RoomList = SimplePagedRoomList(
|
||||
inviteRoomSummariesFlow,
|
||||
inviteRoomsLoadingStateFlow,
|
||||
)
|
||||
|
||||
override fun updateAllRoomsVisibleRange(range: IntRange) {
|
||||
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 syncIndicator: StateFlow<RoomListService.SyncIndicator> = syncIndicatorStateFlow
|
||||
|
||||
@@ -16,11 +16,29 @@
|
||||
|
||||
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.RoomSummary
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
data class SimpleRoomList(
|
||||
data class SimplePagedRoomList(
|
||||
override val summaries: StateFlow<List<RoomSummary>>,
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user