Refactor room list filtering to use Rust SDK
This commit is contained in:
parent
3d75b92ec3
commit
0824a3ab8b
33 changed files with 358 additions and 481 deletions
|
|
@ -24,9 +24,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -55,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -215,17 +214,8 @@ private fun RoomsViewList(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val visibleRange by remember {
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
derivedStateOf {
|
eventSink(RoomListEvent.UpdateVisibleRange(visibleRange))
|
||||||
val layoutInfo = lazyListState.layoutInfo
|
|
||||||
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
|
|
||||||
val size = layoutInfo.visibleItemsInfo.size
|
|
||||||
firstItemIndex until firstItemIndex + size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val updatedEventSink by rememberUpdatedState(newValue = eventSink)
|
|
||||||
LaunchedEffect(visibleRange) {
|
|
||||||
updatedEventSink(RoomListEvent.UpdateVisibleRange(visibleRange))
|
|
||||||
}
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
|
|
@ -237,7 +227,7 @@ private fun RoomsViewList(
|
||||||
item {
|
item {
|
||||||
SetUpRecoveryKeyBanner(
|
SetUpRecoveryKeyBanner(
|
||||||
onContinueClick = onSetUpRecoveryClick,
|
onContinueClick = onSetUpRecoveryClick,
|
||||||
onDismissClick = { updatedEventSink(RoomListEvent.DismissBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +235,7 @@ private fun RoomsViewList(
|
||||||
item {
|
item {
|
||||||
ConfirmRecoveryKeyBanner(
|
ConfirmRecoveryKeyBanner(
|
||||||
onContinueClick = onConfirmRecoveryKeyClick,
|
onContinueClick = onConfirmRecoveryKeyClick,
|
||||||
onDismissClick = { updatedEventSink(RoomListEvent.DismissBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +250,7 @@ private fun RoomsViewList(
|
||||||
} else if (state.showNewNotificationSoundBanner) {
|
} else if (state.showNewNotificationSoundBanner) {
|
||||||
item {
|
item {
|
||||||
NewNotificationSoundBanner(
|
NewNotificationSoundBanner(
|
||||||
onDismissClick = { updatedEventSink(RoomListEvent.DismissNewNotificationSoundBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissNewNotificationSoundBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,33 +9,48 @@
|
||||||
package io.element.android.features.home.impl.datasource
|
package io.element.android.features.home.impl.datasource
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
||||||
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
||||||
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
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 io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 20
|
||||||
|
private const val EXTENDED_VISIBILITY_RANGE_SIZE = 40
|
||||||
|
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
|
||||||
|
private const val PAGINATION_THRESHOLD = 3 * PAGE_SIZE
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@SingleIn(SessionScope::class)
|
||||||
class RoomListDataSource(
|
class RoomListDataSource(
|
||||||
private val roomListService: RoomListService,
|
private val roomListService: RoomListService,
|
||||||
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
|
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
|
||||||
|
|
@ -51,7 +66,12 @@ class RoomListDataSource(
|
||||||
observeDateTimeChanges()
|
observeDateTimeChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _allRooms = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
|
private val roomList = roomListService.createRoomList(
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
source = RoomList.Source.All,
|
||||||
|
coroutineScope = sessionCoroutineScope
|
||||||
|
)
|
||||||
|
private val _roomSummariesFlow = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
|
||||||
|
|
||||||
private val lock = Mutex()
|
private val lock = Mutex()
|
||||||
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
||||||
|
|
@ -59,22 +79,49 @@ class RoomListDataSource(
|
||||||
old?.roomId == new?.roomId
|
old?.roomId == new?.roomId
|
||||||
}
|
}
|
||||||
|
|
||||||
val allRooms: Flow<ImmutableList<RoomListRoomSummary>> = _allRooms
|
val roomSummariesFlow: Flow<ImmutableList<RoomListRoomSummary>> = _roomSummariesFlow
|
||||||
|
|
||||||
val loadingState = roomListService.allRooms.loadingState
|
val loadingState = roomList.loadingState
|
||||||
|
|
||||||
fun launchIn(coroutineScope: CoroutineScope) {
|
fun launchIn(coroutineScope: CoroutineScope) {
|
||||||
roomListService
|
roomList
|
||||||
.allRooms
|
.summaries
|
||||||
.filteredSummaries
|
|
||||||
.onEach { roomSummaries ->
|
.onEach { roomSummaries ->
|
||||||
replaceWith(roomSummaries)
|
replaceWith(roomSummaries)
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
|
suspend fun updateFilter(filter: RoomListFilter) {
|
||||||
roomListService.subscribeToVisibleRooms(roomIds)
|
roomList.updateFilter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateVisibleRange(visibleRange: IntRange) = coroutineScope {
|
||||||
|
launch {
|
||||||
|
roomList.updateVisibleRange(visibleRange, PAGINATION_THRESHOLD)
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
subscribeToVisibleRoomsIfNeeded(visibleRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentSubscribeToVisibleRoomsJob: Job? = null
|
||||||
|
private fun CoroutineScope.subscribeToVisibleRoomsIfNeeded(range: IntRange) {
|
||||||
|
currentSubscribeToVisibleRoomsJob?.cancel()
|
||||||
|
currentSubscribeToVisibleRoomsJob = launch {
|
||||||
|
// Debounce the subscription to avoid subscribing to too many rooms
|
||||||
|
delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS)
|
||||||
|
|
||||||
|
if (range.isEmpty()) return@launch
|
||||||
|
val currentRoomList = roomSummariesFlow.first()
|
||||||
|
// Use extended range to 'prefetch' the next rooms info
|
||||||
|
val midExtendedRangeSize = EXTENDED_VISIBILITY_RANGE_SIZE / 2
|
||||||
|
val extendedRange = range.first until range.last + midExtendedRangeSize
|
||||||
|
val roomIds = extendedRange.mapNotNull { index ->
|
||||||
|
currentRoomList.getOrNull(index)?.roomId
|
||||||
|
}
|
||||||
|
roomListService.subscribeToVisibleRooms(roomIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
|
|
@ -82,7 +129,7 @@ class RoomListDataSource(
|
||||||
notificationSettingsService.notificationSettingsChangeFlow
|
notificationSettingsService.notificationSettingsChangeFlow
|
||||||
.debounce(0.5.seconds)
|
.debounce(0.5.seconds)
|
||||||
.onEach {
|
.onEach {
|
||||||
roomListService.allRooms.rebuildSummaries()
|
roomList.rebuildSummaries()
|
||||||
}
|
}
|
||||||
.launchIn(sessionCoroutineScope)
|
.launchIn(sessionCoroutineScope)
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +155,7 @@ class RoomListDataSource(
|
||||||
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
|
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
|
||||||
// Used to detect duplicates in the room list summaries - see comment below
|
// Used to detect duplicates in the room list summaries - see comment below
|
||||||
data class CacheResult(val index: Int, val fromCache: Boolean)
|
data class CacheResult(val index: Int, val fromCache: Boolean)
|
||||||
|
|
||||||
val cachingResults = mutableMapOf<RoomId, MutableList<CacheResult>>()
|
val cachingResults = mutableMapOf<RoomId, MutableList<CacheResult>>()
|
||||||
|
|
||||||
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
|
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
|
||||||
|
|
@ -144,14 +192,14 @@ class RoomListDataSource(
|
||||||
analyticsService.trackError(
|
analyticsService.trackError(
|
||||||
IllegalStateException(
|
IllegalStateException(
|
||||||
"Found duplicates in room summaries after a local UI update: $duplicates. " +
|
"Found duplicates in room summaries after a local UI update: $duplicates. " +
|
||||||
"This could be a race condition/caching issue of some kind"
|
"This could be a race condition/caching issue of some kind"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove duplicates before emitting the new values
|
// Remove duplicates before emitting the new values
|
||||||
_allRooms.emit(roomListRoomSummaries.distinctBy { it.roomId }.toImmutableList())
|
_roomSummariesFlow.emit(roomListRoomSummaries.distinctBy { it.roomId }.toImmutableList())
|
||||||
} else {
|
} else {
|
||||||
_allRooms.emit(roomListRoomSummaries.toImmutableList())
|
_roomSummariesFlow.emit(roomListRoomSummaries.toImmutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +211,7 @@ class RoomListDataSource(
|
||||||
|
|
||||||
private suspend fun rebuildAllRoomSummaries() {
|
private suspend fun rebuildAllRoomSummaries() {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
roomListService.allRooms.filteredSummaries.replayCache.firstOrNull()?.let { roomSummaries ->
|
roomList.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
|
||||||
buildAndEmitAllRooms(roomSummaries, useCache = false)
|
buildAndEmitAllRooms(roomSummaries, useCache = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,19 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
|
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListFiltersPresenter(
|
class RoomListFiltersPresenter(
|
||||||
private val roomListService: RoomListService,
|
private val roomListDataSource: RoomListDataSource,
|
||||||
private val filterSelectionStrategy: FilterSelectionStrategy,
|
private val filterSelectionStrategy: FilterSelectionStrategy,
|
||||||
) : Presenter<RoomListFiltersState> {
|
) : Presenter<RoomListFiltersState> {
|
||||||
private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
|
private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
|
||||||
|
|
@ -56,9 +59,9 @@ class RoomListFiltersPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.collect { filters ->
|
.collectLatest { filters ->
|
||||||
val result = MatrixRoomListFilter.All(filters)
|
val result = MatrixRoomListFilter.All(filters)
|
||||||
roomListService.allRooms.updateFilter(result)
|
roomListDataSource.updateFilter(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -68,9 +66,6 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private const val EXTENDED_RANGE_SIZE = 40
|
|
||||||
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListPresenter(
|
class RoomListPresenter(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
|
|
@ -119,7 +114,7 @@ class RoomListPresenter(
|
||||||
fun handleEvent(event: RoomListEvent) {
|
fun handleEvent(event: RoomListEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is RoomListEvent.UpdateVisibleRange -> coroutineScope.launch {
|
is RoomListEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
updateVisibleRange(event.range)
|
roomListDataSource.updateVisibleRange(event.range)
|
||||||
}
|
}
|
||||||
RoomListEvent.DismissRequestVerificationPrompt -> securityBannerDismissed = true
|
RoomListEvent.DismissRequestVerificationPrompt -> securityBannerDismissed = true
|
||||||
RoomListEvent.DismissBanner -> securityBannerDismissed = true
|
RoomListEvent.DismissBanner -> securityBannerDismissed = true
|
||||||
|
|
@ -217,7 +212,7 @@ class RoomListPresenter(
|
||||||
showNewNotificationSoundBanner: Boolean,
|
showNewNotificationSoundBanner: Boolean,
|
||||||
): RoomListContentState {
|
): RoomListContentState {
|
||||||
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
||||||
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) }
|
||||||
}
|
}
|
||||||
val loadingState by roomListDataSource.loadingState.collectAsState()
|
val loadingState by roomListDataSource.loadingState.collectAsState()
|
||||||
val showEmpty by remember {
|
val showEmpty by remember {
|
||||||
|
|
@ -323,22 +318,5 @@ class RoomListPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentUpdateVisibleRangeJob: Job? = null
|
|
||||||
private fun CoroutineScope.updateVisibleRange(range: IntRange) {
|
|
||||||
currentUpdateVisibleRangeJob?.cancel()
|
|
||||||
currentUpdateVisibleRangeJob = launch {
|
|
||||||
// Debounce the subscription to avoid subscribing to too many rooms
|
|
||||||
delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS)
|
|
||||||
|
|
||||||
if (range.isEmpty()) return@launch
|
|
||||||
val currentRoomList = roomListDataSource.allRooms.first()
|
|
||||||
// Use extended range to 'prefetch' the next rooms info
|
|
||||||
val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2
|
|
||||||
val extendedRange = range.first until range.last + midExtendedRangeSize
|
|
||||||
val roomIds = extendedRange.mapNotNull { index ->
|
|
||||||
currentRoomList.getOrNull(index)?.roomId
|
|
||||||
}
|
|
||||||
roomListDataSource.subscribeToVisibleRooms(roomIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
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.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
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.loadAllIncrementally
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -42,12 +42,11 @@ class RoomListSearchDataSource(
|
||||||
|
|
||||||
private val roomList = roomListService.createRoomList(
|
private val roomList = roomListService.createRoomList(
|
||||||
pageSize = PAGE_SIZE,
|
pageSize = PAGE_SIZE,
|
||||||
initialFilter = RoomListFilter.None,
|
|
||||||
source = RoomList.Source.All,
|
source = RoomList.Source.All,
|
||||||
coroutineScope = coroutineScope
|
coroutineScope = coroutineScope
|
||||||
)
|
)
|
||||||
|
|
||||||
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.filteredSummaries
|
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.summaries
|
||||||
.map { roomSummaries ->
|
.map { roomSummaries ->
|
||||||
roomSummaries
|
roomSummaries
|
||||||
.map(roomSummaryFactory::create)
|
.map(roomSummaryFactory::create)
|
||||||
|
|
@ -55,12 +54,8 @@ class RoomListSearchDataSource(
|
||||||
}
|
}
|
||||||
.flowOn(coroutineDispatchers.computation)
|
.flowOn(coroutineDispatchers.computation)
|
||||||
|
|
||||||
suspend fun setIsActive(isActive: Boolean) = coroutineScope {
|
suspend fun updateVisibleRange(visibleRange: IntRange) {
|
||||||
if (isActive) {
|
roomList.updateVisibleRange(visibleRange)
|
||||||
roomList.loadAllIncrementally(this)
|
|
||||||
} else {
|
|
||||||
roomList.reset()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
||||||
|
|
@ -71,4 +66,5 @@ class RoomListSearchDataSource(
|
||||||
}
|
}
|
||||||
roomList.updateFilter(filter)
|
roomList.updateFilter(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ package io.element.android.features.home.impl.search
|
||||||
sealed interface RoomListSearchEvent {
|
sealed interface RoomListSearchEvent {
|
||||||
data object ToggleSearchVisibility : RoomListSearchEvent
|
data object ToggleSearchVisibility : RoomListSearchEvent
|
||||||
data object ClearQuery : RoomListSearchEvent
|
data object ClearQuery : RoomListSearchEvent
|
||||||
|
data class UpdateVisibleRange(val range: IntRange) : RoomListSearchEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListSearchPresenter(
|
class RoomListSearchPresenter(
|
||||||
|
|
@ -37,10 +38,6 @@ class RoomListSearchPresenter(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
||||||
|
|
||||||
LaunchedEffect(isSearchActive) {
|
|
||||||
dataSource.setIsActive(isSearchActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(searchQuery.text) {
|
LaunchedEffect(searchQuery.text) {
|
||||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +51,9 @@ class RoomListSearchPresenter(
|
||||||
isSearchActive = !isSearchActive
|
isSearchActive = !isSearchActive
|
||||||
searchQuery.clearText()
|
searchQuery.clearText()
|
||||||
}
|
}
|
||||||
|
is RoomListSearchEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
|
dataSource.updateVisibleRange(visibleRange = event.range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -47,6 +48,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
|
@ -154,7 +156,12 @@ private fun RoomListSearchContent(
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
|
state.eventSink(RoomListSearchEvent.UpdateVisibleRange(visibleRange))
|
||||||
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class RoomListDataSourceTest {
|
||||||
dateTimeObserver = dateTimeObserver,
|
dateTimeObserver = dateTimeObserver,
|
||||||
)
|
)
|
||||||
|
|
||||||
roomListDataSource.allRooms.test {
|
roomListDataSource.roomSummariesFlow.test {
|
||||||
// Observe room list items changes
|
// Observe room list items changes
|
||||||
roomListDataSource.launchIn(backgroundScope)
|
roomListDataSource.launchIn(backgroundScope)
|
||||||
// Get the initial room list
|
// Get the initial room list
|
||||||
|
|
@ -75,7 +75,7 @@ class RoomListDataSourceTest {
|
||||||
),
|
),
|
||||||
dateTimeObserver = dateTimeObserver,
|
dateTimeObserver = dateTimeObserver,
|
||||||
)
|
)
|
||||||
roomListDataSource.allRooms.test {
|
roomListDataSource.roomSummariesFlow.test {
|
||||||
// Observe room list items changes
|
// Observe room list items changes
|
||||||
roomListDataSource.launchIn(backgroundScope)
|
roomListDataSource.launchIn(backgroundScope)
|
||||||
// Get the initial room list
|
// Get the initial room list
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,26 @@
|
||||||
package io.element.android.features.home.impl.filters
|
package io.element.android.features.home.impl.filters
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.home.impl.FakeDateTimeObserver
|
||||||
|
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
||||||
|
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
|
||||||
import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy
|
import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy
|
||||||
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
|
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
|
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
||||||
|
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
|
||||||
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
||||||
|
|
@ -39,13 +53,13 @@ class RoomListFiltersPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun `present - toggle rooms filter`() = runTest {
|
fun `present - toggle rooms filter`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomListService = FakeRoomListService()
|
||||||
val presenter = createRoomListFiltersPresenter(roomListService)
|
val presenter = createRoomListFiltersPresenter(roomListService)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
|
|
||||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||||
assertThat(state.filterSelectionStates).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
filterSelectionState(RoomListFilter.Rooms, true),
|
filterSelectionState(RoomListFilter.Rooms, true),
|
||||||
|
|
@ -56,12 +70,9 @@ class RoomListFiltersPresenterTest {
|
||||||
assertThat(state.selectedFilters()).containsExactly(
|
assertThat(state.selectedFilters()).containsExactly(
|
||||||
RoomListFilter.Rooms,
|
RoomListFilter.Rooms,
|
||||||
)
|
)
|
||||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
|
||||||
assertThat(roomListCurrentFilter.filters).containsExactly(
|
|
||||||
MatrixRoomListFilter.Category.Group,
|
|
||||||
)
|
|
||||||
state.eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
state.eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
||||||
}
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
assertThat(state.filterSelectionStates).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
|
|
@ -72,13 +83,12 @@ class RoomListFiltersPresenterTest {
|
||||||
filterSelectionState(RoomListFilter.Invites, false),
|
filterSelectionState(RoomListFilter.Invites, false),
|
||||||
).inOrder()
|
).inOrder()
|
||||||
assertThat(state.selectedFilters()).isEmpty()
|
assertThat(state.selectedFilters()).isEmpty()
|
||||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
|
||||||
assertThat(roomListCurrentFilter.filters).isEmpty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun `present - clear filters event`() = runTest {
|
fun `present - clear filters event`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomListService = FakeRoomListService()
|
||||||
val presenter = createRoomListFiltersPresenter(roomListService)
|
val presenter = createRoomListFiltersPresenter(roomListService)
|
||||||
|
|
@ -88,6 +98,7 @@ class RoomListFiltersPresenterTest {
|
||||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||||
state.eventSink.invoke(RoomListFiltersEvent.ClearSelectedFilters)
|
state.eventSink.invoke(RoomListFiltersEvent.ClearSelectedFilters)
|
||||||
}
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
}
|
}
|
||||||
|
|
@ -100,11 +111,25 @@ private fun filterSelectionState(filter: RoomListFilter, selected: Boolean) = Fi
|
||||||
isSelected = selected,
|
isSelected = selected,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createRoomListFiltersPresenter(
|
private fun TestScope.createRoomListFiltersPresenter(
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
|
notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||||
|
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||||
|
roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
||||||
): RoomListFiltersPresenter {
|
): RoomListFiltersPresenter {
|
||||||
return RoomListFiltersPresenter(
|
return RoomListFiltersPresenter(
|
||||||
roomListService = roomListService,
|
roomListDataSource = RoomListDataSource(
|
||||||
|
roomListService = roomListService,
|
||||||
|
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||||
|
dateFormatter = dateFormatter,
|
||||||
|
roomLatestEventFormatter = roomLatestEventFormatter,
|
||||||
|
),
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
notificationSettingsService = notificationSettingsService,
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
|
dateTimeObserver = FakeDateTimeObserver(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
),
|
||||||
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,5 @@ sealed interface AddRoomToSpaceEvent {
|
||||||
data object Save : AddRoomToSpaceEvent
|
data object Save : AddRoomToSpaceEvent
|
||||||
data object ResetSaveAction : AddRoomToSpaceEvent
|
data object ResetSaveAction : AddRoomToSpaceEvent
|
||||||
data object Dismiss : AddRoomToSpaceEvent
|
data object Dismiss : AddRoomToSpaceEvent
|
||||||
|
data class UpdateSearchVisibleRange(val range: IntRange) : AddRoomToSpaceEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,6 @@ class AddRoomToSpacePresenter(
|
||||||
LaunchedEffect(searchQuery.text) {
|
LaunchedEffect(searchQuery.text) {
|
||||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||||
}
|
}
|
||||||
LaunchedEffect(isSearchActive) {
|
|
||||||
dataSource.setIsActive(isSearchActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf())
|
val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf())
|
||||||
|
|
||||||
|
|
@ -111,6 +108,9 @@ class AddRoomToSpacePresenter(
|
||||||
coroutineScope.launch { spaceRoomList.reset() }
|
coroutineScope.launch { spaceRoomList.reset() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is AddRoomToSpaceEvent.UpdateSearchVisibleRange -> coroutineScope.launch {
|
||||||
|
dataSource.updateVisibleRange(event.range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,13 @@ import io.element.android.libraries.matrix.api.room.recent.getRecentlyVisitedRoo
|
||||||
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.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
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.loadAllIncrementally
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
@ -58,7 +57,6 @@ class AddRoomToSpaceSearchDataSource(
|
||||||
|
|
||||||
private val roomList = roomListService.createRoomList(
|
private val roomList = roomListService.createRoomList(
|
||||||
pageSize = PAGE_SIZE,
|
pageSize = PAGE_SIZE,
|
||||||
initialFilter = RoomListFilter.all(),
|
|
||||||
source = RoomList.Source.All,
|
source = RoomList.Source.All,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
)
|
)
|
||||||
|
|
@ -87,7 +85,7 @@ class AddRoomToSpaceSearchDataSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
||||||
roomList.filteredSummaries,
|
roomList.summaries,
|
||||||
spaceChildrenFlow,
|
spaceChildrenFlow,
|
||||||
addedRoomIdsFlow,
|
addedRoomIdsFlow,
|
||||||
) { roomSummaries, childIds, addedIds ->
|
) { roomSummaries, childIds, addedIds ->
|
||||||
|
|
@ -109,12 +107,8 @@ class AddRoomToSpaceSearchDataSource(
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}.flowOn(coroutineDispatchers.computation)
|
}.flowOn(coroutineDispatchers.computation)
|
||||||
|
|
||||||
suspend fun setIsActive(isActive: Boolean) = coroutineScope {
|
suspend fun updateVisibleRange(visibleRange: IntRange) {
|
||||||
if (isActive) {
|
roomList.updateVisibleRange(visibleRange)
|
||||||
roomList.loadAllIncrementally(this)
|
|
||||||
} else {
|
|
||||||
roomList.reset()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setSearchQuery(searchQuery: String) {
|
suspend fun setSearchQuery(searchQuery: String) {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -43,6 +44,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
|
@ -121,6 +123,10 @@ fun AddRoomToSpaceView(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { rooms ->
|
) { rooms ->
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
|
state.eventSink(AddRoomToSpaceEvent.UpdateSearchVisibleRange(visibleRange))
|
||||||
|
}
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(rooms, key = { it.roomId }) { roomInfo ->
|
items(rooms, key = { it.roomId }) { roomInfo ->
|
||||||
RoomListItem(
|
RoomListItem(
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,17 @@ import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the lazy list is currently scrolling up.
|
* Returns whether the lazy list is currently scrolling up.
|
||||||
|
|
@ -73,3 +79,20 @@ suspend fun LazyListState.animateScrollToItemCenter(index: Int) {
|
||||||
animateScrollToItem(index, offset)
|
animateScrollToItem(index, offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OnVisibleRangeChangeEffect(lazyListState: LazyListState, onChange: (IntRange) -> Unit) {
|
||||||
|
val onChangeUpdated by rememberUpdatedState(onChange)
|
||||||
|
LaunchedEffect(lazyListState) {
|
||||||
|
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
|
||||||
|
.map { visibleItemsInfo ->
|
||||||
|
val firstItemIndex = visibleItemsInfo.firstOrNull()?.index ?: 0
|
||||||
|
val size = visibleItemsInfo.size
|
||||||
|
firstItemIndex until firstItemIndex + size
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collectLatest { visibleRange ->
|
||||||
|
onChangeUpdated(visibleRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.roomlist
|
package io.element.android.libraries.matrix.api.roomlist
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import timber.log.Timber
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RoomList with dynamic filtering and loading.
|
* RoomList with dynamic filtering and loading.
|
||||||
|
|
@ -21,12 +16,8 @@ import kotlinx.coroutines.flow.onEach
|
||||||
* It lets load rooms on demand and filter them.
|
* It lets load rooms on demand and filter them.
|
||||||
*/
|
*/
|
||||||
interface DynamicRoomList : RoomList {
|
interface DynamicRoomList : RoomList {
|
||||||
val currentFilter: StateFlow<RoomListFilter>
|
|
||||||
val loadedPages: StateFlow<Int>
|
|
||||||
val pageSize: Int
|
val pageSize: Int
|
||||||
|
|
||||||
val filteredSummaries: SharedFlow<List<RoomSummary>>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load more rooms into the list if possible.
|
* Load more rooms into the list if possible.
|
||||||
*/
|
*/
|
||||||
|
|
@ -44,28 +35,13 @@ interface DynamicRoomList : RoomList {
|
||||||
suspend fun updateFilter(filter: RoomListFilter)
|
suspend fun updateFilter(filter: RoomListFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun DynamicRoomList.updateVisibleRange(
|
||||||
* Offers a way to load all the rooms incrementally.
|
visibleRange: IntRange,
|
||||||
* It will load more room until all are loaded.
|
paginationThreshold: Int = pageSize * 3
|
||||||
* If total number of rooms increase, it will load more pages if needed.
|
) {
|
||||||
* The number of rooms is independent of the filter.
|
val loadedCount = summaries.replayCache.firstOrNull().orEmpty().count()
|
||||||
*/
|
val threshold = loadedCount - paginationThreshold
|
||||||
fun DynamicRoomList.loadAllIncrementally(coroutineScope: CoroutineScope) {
|
if (visibleRange.last >= threshold) {
|
||||||
combine(
|
loadMore()
|
||||||
loadedPages,
|
|
||||||
loadingState,
|
|
||||||
) { loadedPages, loadingState ->
|
|
||||||
loadedPages to loadingState
|
|
||||||
}
|
}
|
||||||
.onEach { (loadedPages, loadingState) ->
|
|
||||||
when (loadingState) {
|
|
||||||
is RoomList.LoadingState.Loaded -> {
|
|
||||||
if (pageSize * loadedPages < loadingState.numberOfRooms) {
|
|
||||||
loadMore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RoomList.LoadingState.NotLoaded -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchIn(coroutineScope)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import kotlin.time.Duration
|
||||||
* Can be retrieved from [RoomListService] methods.
|
* Can be retrieved from [RoomListService] methods.
|
||||||
*/
|
*/
|
||||||
interface RoomList {
|
interface RoomList {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The loading state of the room list.
|
* The loading state of the room list.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.roomlist
|
package io.element.android.libraries.matrix.api.roomlist
|
||||||
|
|
||||||
import io.element.android.libraries.core.extensions.withoutAccents
|
|
||||||
|
|
||||||
sealed interface RoomListFilter {
|
sealed interface RoomListFilter {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,7 +75,5 @@ sealed interface RoomListFilter {
|
||||||
*/
|
*/
|
||||||
data class NormalizedMatchRoomName(
|
data class NormalizedMatchRoomName(
|
||||||
val pattern: String
|
val pattern: String
|
||||||
) : RoomListFilter {
|
) : RoomListFilter
|
||||||
val normalizedPattern: String = pattern.withoutAccents()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,11 @@ interface RoomListService {
|
||||||
/**
|
/**
|
||||||
* Creates a room list that can be used to load more rooms and filter them dynamically.
|
* Creates a room list that can be used to load more rooms and filter them dynamically.
|
||||||
* @param pageSize the number of rooms to load at once.
|
* @param pageSize the number of rooms to load at once.
|
||||||
* @param initialFilter the initial filter to apply to the rooms.
|
|
||||||
* @param source the source of the rooms, either all rooms or invites.
|
* @param source the source of the rooms, either all rooms or invites.
|
||||||
* @param coroutineScope the coroutine scope to use for the room list operations.
|
* @param coroutineScope the coroutine scope to use for the room list operations.
|
||||||
*/
|
*/
|
||||||
fun createRoomList(
|
fun createRoomList(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
initialFilter: RoomListFilter,
|
|
||||||
source: RoomList.Source,
|
source: RoomList.Source,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
): DynamicRoomList
|
): DynamicRoomList
|
||||||
|
|
@ -56,10 +54,10 @@ interface RoomListService {
|
||||||
suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>)
|
suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [DynamicRoomList] object of all rooms we want to display.
|
* Returns a [RoomList] object with all rooms locally known.
|
||||||
* If you want to get a filtered room list, consider using [createRoomList].
|
* If you want to get a filtered room list, consider using [createRoomList].
|
||||||
*/
|
*/
|
||||||
val allRooms: DynamicRoomList
|
val allRooms: RoomList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sync indicator as a flow.
|
* The sync indicator as a flow.
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2023-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.roomlist
|
|
||||||
|
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
|
||||||
|
|
||||||
internal sealed interface RoomListDynamicEvents {
|
|
||||||
data object Reset : RoomListDynamicEvents
|
|
||||||
data object LoadMore : RoomListDynamicEvents
|
|
||||||
data class SetFilter(val filter: RoomListEntriesDynamicFilterKind) : RoomListDynamicEvents
|
|
||||||
}
|
|
||||||
|
|
@ -21,9 +21,10 @@ import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController
|
||||||
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.RoomListEntriesDynamicFilterKind
|
||||||
import org.matrix.rustcomponents.sdk.RoomListInterface
|
import org.matrix.rustcomponents.sdk.RoomListInterface
|
||||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||||
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
|
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
|
||||||
|
|
@ -57,8 +58,8 @@ fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
|
||||||
|
|
||||||
internal fun RoomListInterface.entriesFlow(
|
internal fun RoomListInterface.entriesFlow(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
roomListDynamicEvents: Flow<RoomListDynamicEvents>,
|
initialFilterKind: RoomListEntriesDynamicFilterKind,
|
||||||
initialFilterKind: RoomListEntriesDynamicFilterKind
|
onControllerCreated: (RoomListDynamicEntriesController) -> Unit,
|
||||||
): Flow<List<RoomListEntriesUpdate>> =
|
): Flow<List<RoomListEntriesUpdate>> =
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
val listener = object : RoomListEntriesListener {
|
val listener = object : RoomListEntriesListener {
|
||||||
|
|
@ -73,19 +74,7 @@ internal fun RoomListInterface.entriesFlow(
|
||||||
)
|
)
|
||||||
val controller = result.controller()
|
val controller = result.controller()
|
||||||
controller.setFilter(initialFilterKind)
|
controller.setFilter(initialFilterKind)
|
||||||
roomListDynamicEvents.onEach { controllerEvents ->
|
onControllerCreated(controller)
|
||||||
when (controllerEvents) {
|
|
||||||
is RoomListDynamicEvents.SetFilter -> {
|
|
||||||
controller.setFilter(controllerEvents.filter)
|
|
||||||
}
|
|
||||||
is RoomListDynamicEvents.LoadMore -> {
|
|
||||||
controller.addOnePage()
|
|
||||||
}
|
|
||||||
is RoomListDynamicEvents.Reset -> {
|
|
||||||
controller.resetToOnePage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.launchIn(this)
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
result.entriesStream().cancelAndDestroy()
|
result.entriesStream().cancelAndDestroy()
|
||||||
controller.destroy()
|
controller.destroy()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ 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.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.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter.Companion.all
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
@ -18,24 +19,16 @@ import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||||
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.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
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.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController
|
||||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||||
import org.matrix.rustcomponents.sdk.RoomListService
|
import org.matrix.rustcomponents.sdk.RoomListService
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
|
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
|
||||||
|
|
||||||
private val ROOM_LIST_RUST_FILTERS = listOf(
|
|
||||||
RoomListEntriesDynamicFilterKind.NonLeft,
|
|
||||||
RoomListEntriesDynamicFilterKind.DeduplicateVersions
|
|
||||||
)
|
|
||||||
|
|
||||||
internal class RoomListFactory(
|
internal class RoomListFactory(
|
||||||
private val innerRoomListService: RoomListService,
|
private val innerRoomListService: RoomListService,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
|
|
@ -49,18 +42,14 @@ internal class RoomListFactory(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
coroutineContext: CoroutineContext,
|
coroutineContext: CoroutineContext,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
initialFilter: RoomListFilter = RoomListFilter.all(),
|
initialFilter: RoomListFilter = all(),
|
||||||
innerProvider: suspend () -> InnerRoomList
|
innerProvider: suspend () -> InnerRoomList
|
||||||
): DynamicRoomList {
|
): DynamicRoomList {
|
||||||
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
|
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
|
||||||
val filteredSummariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
|
|
||||||
val summariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
|
val summariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
|
||||||
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory, analyticsService)
|
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory, analyticsService)
|
||||||
// Makes sure we don't miss any events
|
|
||||||
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>(replay = 100)
|
|
||||||
val currentFilter = MutableStateFlow(initialFilter)
|
|
||||||
val loadedPages = MutableStateFlow(1)
|
|
||||||
var innerRoomList: InnerRoomList? = null
|
var innerRoomList: InnerRoomList? = null
|
||||||
|
var dynamicController: RoomListDynamicEntriesController? = null
|
||||||
|
|
||||||
val firstRoomsTransaction = analyticsService.startTransaction("Load first set of rooms", "innerRoomList.entriesFlow")
|
val firstRoomsTransaction = analyticsService.startTransaction("Load first set of rooms", "innerRoomList.entriesFlow")
|
||||||
|
|
||||||
|
|
@ -69,8 +58,10 @@ internal class RoomListFactory(
|
||||||
innerRoomList.let { innerRoomList ->
|
innerRoomList.let { innerRoomList ->
|
||||||
innerRoomList.entriesFlow(
|
innerRoomList.entriesFlow(
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
roomListDynamicEvents = dynamicEvents,
|
initialFilterKind = RoomListFilterMapper.toRustFilter(initialFilter),
|
||||||
initialFilterKind = RoomListEntriesDynamicFilterKind.All(ROOM_LIST_RUST_FILTERS),
|
onControllerCreated = { controller ->
|
||||||
|
dynamicController = controller
|
||||||
|
}
|
||||||
).onEach { update ->
|
).onEach { update ->
|
||||||
if (!firstRoomsTransaction.isFinished()) {
|
if (!firstRoomsTransaction.isFinished()) {
|
||||||
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.FirstRoomsDisplayed)
|
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.FirstRoomsDisplayed)
|
||||||
|
|
@ -85,60 +76,21 @@ internal class RoomListFactory(
|
||||||
loadingStateFlow.value = it
|
loadingStateFlow.value = it
|
||||||
}
|
}
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
|
|
||||||
combine(
|
|
||||||
currentFilter,
|
|
||||||
summariesFlow
|
|
||||||
) { filter, summaries ->
|
|
||||||
summaries.filter(filter)
|
|
||||||
}.onEach {
|
|
||||||
filteredSummariesFlow.emit(it)
|
|
||||||
}.launchIn(this)
|
|
||||||
}
|
}
|
||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
innerRoomList?.destroy()
|
innerRoomList?.destroy()
|
||||||
}
|
}
|
||||||
return RustDynamicRoomList(
|
return RustDynamicRoomList(
|
||||||
summaries = summariesFlow,
|
summaries = summariesFlow,
|
||||||
filteredSummaries = filteredSummariesFlow,
|
|
||||||
loadingState = loadingStateFlow,
|
loadingState = loadingStateFlow,
|
||||||
currentFilter = currentFilter,
|
|
||||||
loadedPages = loadedPages,
|
|
||||||
dynamicEvents = dynamicEvents,
|
|
||||||
processor = processor,
|
processor = processor,
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
|
dynamicController = { dynamicController }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RustDynamicRoomList(
|
|
||||||
override val summaries: MutableSharedFlow<List<RoomSummary>>,
|
|
||||||
override val filteredSummaries: SharedFlow<List<RoomSummary>>,
|
|
||||||
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
|
||||||
override val currentFilter: MutableStateFlow<RoomListFilter>,
|
|
||||||
override val loadedPages: MutableStateFlow<Int>,
|
|
||||||
private val dynamicEvents: MutableSharedFlow<RoomListDynamicEvents>,
|
|
||||||
private val processor: RoomSummaryListProcessor,
|
|
||||||
override val pageSize: Int,
|
|
||||||
) : DynamicRoomList {
|
|
||||||
override suspend fun rebuildSummaries() {
|
|
||||||
processor.rebuildRoomSummaries()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateFilter(filter: RoomListFilter) {
|
|
||||||
currentFilter.emit(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun loadMore() {
|
|
||||||
dynamicEvents.emit(RoomListDynamicEvents.LoadMore)
|
|
||||||
loadedPages.getAndUpdate { it + 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun reset() {
|
|
||||||
dynamicEvents.emit(RoomListDynamicEvents.Reset)
|
|
||||||
loadedPages.emit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
|
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2024, 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.roomlist
|
|
||||||
|
|
||||||
import io.element.android.libraries.core.extensions.withoutAccents
|
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
|
|
||||||
val RoomListFilter.predicate
|
|
||||||
get() = when (this) {
|
|
||||||
is RoomListFilter.All -> { roomSummary -> NonSpacePredicate(roomSummary) || IsInvitedPredicate(roomSummary) }
|
|
||||||
is RoomListFilter.Any -> { roomSummary -> NonSpacePredicate(roomSummary) || IsInvitedPredicate(roomSummary) }
|
|
||||||
RoomListFilter.None -> { _ -> false }
|
|
||||||
RoomListFilter.Category.Group -> { roomSummary: RoomSummary ->
|
|
||||||
!roomSummary.info.isDm && NonInvitedPredicate(roomSummary) && NonSpacePredicate(roomSummary)
|
|
||||||
}
|
|
||||||
RoomListFilter.Category.People -> { roomSummary: RoomSummary ->
|
|
||||||
roomSummary.info.isDm && NonInvitedPredicate(roomSummary) && NonSpacePredicate(roomSummary)
|
|
||||||
}
|
|
||||||
RoomListFilter.Category.Space -> IsSpacePredicate
|
|
||||||
RoomListFilter.Favorite -> { roomSummary: RoomSummary ->
|
|
||||||
roomSummary.info.isFavorite && NonInvitedPredicate(roomSummary) && NonSpacePredicate(roomSummary)
|
|
||||||
}
|
|
||||||
RoomListFilter.Unread -> { roomSummary: RoomSummary ->
|
|
||||||
NonInvitedPredicate(roomSummary) &&
|
|
||||||
NonSpacePredicate(roomSummary) &&
|
|
||||||
(roomSummary.info.numUnreadNotifications > 0 || roomSummary.info.isMarkedUnread)
|
|
||||||
}
|
|
||||||
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
|
|
||||||
roomSummary.info.name?.withoutAccents().orEmpty().contains(normalizedPattern, ignoreCase = true) &&
|
|
||||||
(NonSpacePredicate(roomSummary) || IsInvitedPredicate(roomSummary))
|
|
||||||
}
|
|
||||||
RoomListFilter.Invite -> IsInvitedPredicate
|
|
||||||
}
|
|
||||||
|
|
||||||
fun List<RoomSummary>.filter(filter: RoomListFilter): List<RoomSummary> {
|
|
||||||
return when (filter) {
|
|
||||||
is RoomListFilter.All -> {
|
|
||||||
val predicates = if (filter.filters.isNotEmpty()) {
|
|
||||||
filter.filters.map { it.predicate }
|
|
||||||
} else {
|
|
||||||
listOf(filter.predicate)
|
|
||||||
}
|
|
||||||
filter { roomSummary -> predicates.all { it(roomSummary) } }
|
|
||||||
}
|
|
||||||
is RoomListFilter.Any -> {
|
|
||||||
val predicates = if (filter.filters.isNotEmpty()) {
|
|
||||||
filter.filters.map { it.predicate }
|
|
||||||
} else {
|
|
||||||
listOf(filter.predicate)
|
|
||||||
}
|
|
||||||
filter { roomSummary -> predicates.any { it(roomSummary) } }
|
|
||||||
}
|
|
||||||
else -> filter(filter.predicate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val IsSpacePredicate = { roomSummary: RoomSummary -> roomSummary.info.isSpace }
|
|
||||||
|
|
||||||
private val NonSpacePredicate = { roomSummary: RoomSummary -> !IsSpacePredicate(roomSummary) }
|
|
||||||
|
|
||||||
private val IsInvitedPredicate = { roomSummary: RoomSummary -> roomSummary.info.currentUserMembership == CurrentUserMembership.INVITED }
|
|
||||||
|
|
||||||
private val NonInvitedPredicate = { roomSummary: RoomSummary -> !IsInvitedPredicate(roomSummary) }
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.roomlist
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.All
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Any
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Category
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.DeduplicateVersions
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Favourite
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Invite
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonLeft
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonSpace
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.None
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Space
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Unread
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListFilterCategory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper for converting RoomListFilter to Rust SDK filter kinds.
|
||||||
|
*/
|
||||||
|
internal object RoomListFilterMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base rust filters to always apply across all room lists.
|
||||||
|
* These filters ensure we show:
|
||||||
|
* - Non-space, non-left rooms (regular rooms user is part of)
|
||||||
|
* - OR space invites (pending space invitations)
|
||||||
|
* - With version deduplication enabled
|
||||||
|
*/
|
||||||
|
private val RUST_BASE_FILTERS = listOf(
|
||||||
|
Any(
|
||||||
|
listOf(
|
||||||
|
All(listOf(NonSpace, NonLeft)),
|
||||||
|
All(listOf(Space, Invite)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
DeduplicateVersions
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a RoomListFilter to a Rust SDK RoomListEntriesDynamicFilterKind.
|
||||||
|
* Applies base filters along with the provided filter.
|
||||||
|
*/
|
||||||
|
fun toRustFilter(filter: RoomListFilter): RoomListEntriesDynamicFilterKind {
|
||||||
|
return All(RUST_BASE_FILTERS + mapFilter(filter))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a RoomListFilter to its Rust SDK equivalent.
|
||||||
|
* This replaces the previous RoomListFilter.into() extension function.
|
||||||
|
*/
|
||||||
|
private fun mapFilter(filter: RoomListFilter): RoomListEntriesDynamicFilterKind {
|
||||||
|
return when (filter) {
|
||||||
|
is RoomListFilter.All -> All(filters = filter.filters.map { mapFilter(it) })
|
||||||
|
is RoomListFilter.Any -> Any(filters = filter.filters.map { mapFilter(it) })
|
||||||
|
RoomListFilter.None -> None
|
||||||
|
RoomListFilter.Category.Group -> Category(RoomListFilterCategory.GROUP)
|
||||||
|
RoomListFilter.Category.People -> Category(RoomListFilterCategory.PEOPLE)
|
||||||
|
RoomListFilter.Category.Space -> Space
|
||||||
|
RoomListFilter.Favorite -> Favourite
|
||||||
|
RoomListFilter.Unread -> Unread
|
||||||
|
is RoomListFilter.NormalizedMatchRoomName -> NormalizedMatchRoomName(
|
||||||
|
pattern = filter.pattern
|
||||||
|
)
|
||||||
|
RoomListFilter.Invite -> Invite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.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.RoomListFilter
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController
|
||||||
|
|
||||||
|
private const val DEFAULT_ADD_PAGES_COUNT = 3
|
||||||
|
|
||||||
|
internal class RustDynamicRoomList(
|
||||||
|
override val summaries: MutableSharedFlow<List<RoomSummary>>,
|
||||||
|
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
||||||
|
private val processor: RoomSummaryListProcessor,
|
||||||
|
override val pageSize: Int,
|
||||||
|
private val dynamicController: () -> RoomListDynamicEntriesController?,
|
||||||
|
private val addPagesCount: Int = DEFAULT_ADD_PAGES_COUNT
|
||||||
|
) : DynamicRoomList {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
override suspend fun rebuildSummaries() {
|
||||||
|
processor.rebuildRoomSummaries()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateFilter(filter: RoomListFilter) {
|
||||||
|
mutex.withLock {
|
||||||
|
dynamicController()?.let { controller ->
|
||||||
|
// Reset pagination when filter changes
|
||||||
|
controller.resetToOnePage()
|
||||||
|
val rustFilter = RoomListFilterMapper.toRustFilter(filter)
|
||||||
|
controller.setFilter(rustFilter)
|
||||||
|
// Then preload some pages
|
||||||
|
controller.addPages(addPagesCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadMore() {
|
||||||
|
mutex.withLock {
|
||||||
|
dynamicController()?.addPages(addPagesCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun reset() {
|
||||||
|
mutex.withLock {
|
||||||
|
dynamicController()?.resetToOnePage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomListDynamicEntriesController.addPages(pageCount: Int) = repeat(pageCount) { addOnePage() }
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,7 @@ package io.element.android.libraries.matrix.impl.roomlist
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
|
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.RoomListFilter
|
|
||||||
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.loadAllIncrementally
|
|
||||||
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -28,8 +26,6 @@ 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
|
||||||
|
|
||||||
private const val DEFAULT_PAGE_SIZE = 20
|
|
||||||
|
|
||||||
internal class RustRoomListService(
|
internal class RustRoomListService(
|
||||||
private val innerRoomListService: InnerRustRoomListService,
|
private val innerRoomListService: InnerRustRoomListService,
|
||||||
private val sessionDispatcher: CoroutineDispatcher,
|
private val sessionDispatcher: CoroutineDispatcher,
|
||||||
|
|
@ -39,13 +35,11 @@ internal class RustRoomListService(
|
||||||
) : RoomListService {
|
) : RoomListService {
|
||||||
override fun createRoomList(
|
override fun createRoomList(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
initialFilter: RoomListFilter,
|
|
||||||
source: RoomList.Source,
|
source: RoomList.Source,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
): DynamicRoomList {
|
): DynamicRoomList {
|
||||||
return roomListFactory.createRoomList(
|
return roomListFactory.createRoomList(
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
initialFilter = initialFilter,
|
|
||||||
coroutineContext = sessionDispatcher,
|
coroutineContext = sessionDispatcher,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
) {
|
) {
|
||||||
|
|
@ -59,18 +53,14 @@ internal class RustRoomListService(
|
||||||
roomSyncSubscriber.batchSubscribe(roomIds)
|
roomSyncSubscriber.batchSubscribe(roomIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val allRooms: DynamicRoomList = roomListFactory.createRoomList(
|
override val allRooms: RoomList = roomListFactory.createRoomList(
|
||||||
pageSize = DEFAULT_PAGE_SIZE,
|
pageSize = Int.MAX_VALUE,
|
||||||
coroutineContext = sessionDispatcher,
|
coroutineContext = sessionDispatcher,
|
||||||
coroutineScope = sessionCoroutineScope,
|
coroutineScope = sessionCoroutineScope,
|
||||||
) {
|
) {
|
||||||
innerRoomListService.allRooms()
|
innerRoomListService.allRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
allRooms.loadAllIncrementally(sessionCoroutineScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
|
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
|
||||||
innerRoomListService.syncIndicator()
|
innerRoomListService.syncIndicator()
|
||||||
.map { it.toSyncIndicator() }
|
.map { it.toSyncIndicator() }
|
||||||
|
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2024, 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.roomlist
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class RoomListFilterTest {
|
|
||||||
private val regularRoom = aRoomSummary(
|
|
||||||
isDirect = false,
|
|
||||||
)
|
|
||||||
private val dmRoom = aRoomSummary(
|
|
||||||
isDirect = true,
|
|
||||||
activeMembersCount = 2
|
|
||||||
)
|
|
||||||
private val favoriteRoom = aRoomSummary(
|
|
||||||
isFavorite = true
|
|
||||||
)
|
|
||||||
private val markedAsUnreadRoom = aRoomSummary(
|
|
||||||
isMarkedUnread = true
|
|
||||||
)
|
|
||||||
private val unreadNotificationRoom = aRoomSummary(
|
|
||||||
numUnreadNotifications = 1
|
|
||||||
)
|
|
||||||
private val roomToSearch = aRoomSummary(
|
|
||||||
name = "Room to search"
|
|
||||||
)
|
|
||||||
private val roomWithAccent = aRoomSummary(
|
|
||||||
name = "Frédéric"
|
|
||||||
)
|
|
||||||
private val invitedRoom = aRoomSummary(
|
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
|
||||||
)
|
|
||||||
|
|
||||||
private val space = aRoomSummary(
|
|
||||||
isSpace = true
|
|
||||||
)
|
|
||||||
private val invitedSpace = aRoomSummary(
|
|
||||||
isSpace = true,
|
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
|
||||||
)
|
|
||||||
|
|
||||||
private val roomSummaries = listOf(
|
|
||||||
regularRoom,
|
|
||||||
dmRoom,
|
|
||||||
favoriteRoom,
|
|
||||||
markedAsUnreadRoom,
|
|
||||||
unreadNotificationRoom,
|
|
||||||
roomToSearch,
|
|
||||||
roomWithAccent,
|
|
||||||
invitedRoom,
|
|
||||||
space,
|
|
||||||
invitedSpace,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter all empty`() = runTest {
|
|
||||||
val filter = RoomListFilter.all()
|
|
||||||
assertThat(roomSummaries.filter(filter)).isEqualTo(roomSummaries - space)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter none`() = runTest {
|
|
||||||
val filter = RoomListFilter.None
|
|
||||||
assertThat(roomSummaries.filter(filter)).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter people`() = runTest {
|
|
||||||
val filter = RoomListFilter.Category.People
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(dmRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter group`() = runTest {
|
|
||||||
val filter = RoomListFilter.Category.Group
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(
|
|
||||||
regularRoom,
|
|
||||||
favoriteRoom,
|
|
||||||
markedAsUnreadRoom,
|
|
||||||
unreadNotificationRoom,
|
|
||||||
roomToSearch,
|
|
||||||
roomWithAccent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter space`() = runTest {
|
|
||||||
val filter = RoomListFilter.Category.Space
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(space, invitedSpace)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter favorite`() = runTest {
|
|
||||||
val filter = RoomListFilter.Favorite
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(favoriteRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter unread`() = runTest {
|
|
||||||
val filter = RoomListFilter.Unread
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(markedAsUnreadRoom, unreadNotificationRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter invites`() = runTest {
|
|
||||||
val filter = RoomListFilter.Invite
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(invitedRoom, invitedSpace)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter normalized match room name`() = runTest {
|
|
||||||
val filter = RoomListFilter.NormalizedMatchRoomName("search")
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(roomToSearch)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter normalized match room name with accent`() = runTest {
|
|
||||||
val filter = RoomListFilter.NormalizedMatchRoomName("Fred")
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(roomWithAccent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter normalized match room name with accent when searching with accent`() = runTest {
|
|
||||||
val filter = RoomListFilter.NormalizedMatchRoomName("Fréd")
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(roomWithAccent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter all with one match`() = runTest {
|
|
||||||
val filter = RoomListFilter.all(
|
|
||||||
RoomListFilter.Category.Group,
|
|
||||||
RoomListFilter.Favorite
|
|
||||||
)
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(favoriteRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter all with no match`() = runTest {
|
|
||||||
val filter = RoomListFilter.all(
|
|
||||||
RoomListFilter.Category.People,
|
|
||||||
RoomListFilter.Favorite
|
|
||||||
)
|
|
||||||
assertThat(roomSummaries.filter(filter)).isEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -44,7 +44,6 @@ class FakeRoomListService(
|
||||||
|
|
||||||
override fun createRoomList(
|
override fun createRoomList(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
initialFilter: RoomListFilter,
|
|
||||||
source: RoomList.Source,
|
source: RoomList.Source,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
): DynamicRoomList {
|
): DynamicRoomList {
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,16 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
|
||||||
data class SimplePagedRoomList(
|
data class SimplePagedRoomList(
|
||||||
override val summaries: MutableStateFlow<List<RoomSummary>>,
|
override val summaries: MutableStateFlow<List<RoomSummary>>,
|
||||||
override val loadingState: StateFlow<RoomList.LoadingState>,
|
override val loadingState: StateFlow<RoomList.LoadingState>,
|
||||||
override val currentFilter: MutableStateFlow<RoomListFilter>
|
private val currentFilter: MutableStateFlow<RoomListFilter>
|
||||||
) : DynamicRoomList {
|
) : DynamicRoomList {
|
||||||
override val pageSize: Int = Int.MAX_VALUE
|
override val pageSize: Int = Int.MAX_VALUE
|
||||||
override val loadedPages = MutableStateFlow(1)
|
private val loadedPages = MutableStateFlow(1)
|
||||||
|
|
||||||
override val filteredSummaries: SharedFlow<List<RoomSummary>> = summaries
|
|
||||||
|
|
||||||
override suspend fun loadMore() {
|
override suspend fun loadMore() {
|
||||||
// No-op
|
// No-op
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,5 @@ sealed interface RoomSelectEvents {
|
||||||
// TODO remove to restore multi-selection
|
// TODO remove to restore multi-selection
|
||||||
data object RemoveSelectedRoom : RoomSelectEvents
|
data object RemoveSelectedRoom : RoomSelectEvents
|
||||||
data object ToggleSearchActive : RoomSelectEvents
|
data object ToggleSearchActive : RoomSelectEvents
|
||||||
|
data class UpdateVisibleRange(val range: IntRange): RoomSelectEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class RoomSelectPresenter(
|
class RoomSelectPresenter(
|
||||||
|
|
@ -80,6 +81,9 @@ class RoomSelectPresenter(
|
||||||
}
|
}
|
||||||
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
||||||
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
||||||
|
is RoomSelectEvents.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
|
dataSource.updateVisibleRange(event.range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
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.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
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.loadAllIncrementally
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -46,14 +46,11 @@ class RoomSelectSearchDataSource(
|
||||||
|
|
||||||
private val roomList = roomListService.createRoomList(
|
private val roomList = roomListService.createRoomList(
|
||||||
pageSize = PAGE_SIZE,
|
pageSize = PAGE_SIZE,
|
||||||
initialFilter = RoomListFilter.all(),
|
|
||||||
source = RoomList.Source.All,
|
source = RoomList.Source.All,
|
||||||
coroutineScope = coroutineScope
|
coroutineScope = coroutineScope
|
||||||
).apply {
|
)
|
||||||
loadAllIncrementally(coroutineScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = roomList.filteredSummaries
|
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = roomList.summaries
|
||||||
.map { roomSummaries ->
|
.map { roomSummaries ->
|
||||||
roomSummaries
|
roomSummaries
|
||||||
.filter { it.info.currentUserMembership == CurrentUserMembership.JOINED }
|
.filter { it.info.currentUserMembership == CurrentUserMembership.JOINED }
|
||||||
|
|
@ -63,6 +60,10 @@ class RoomSelectSearchDataSource(
|
||||||
}
|
}
|
||||||
.flowOn(coroutineDispatchers.computation)
|
.flowOn(coroutineDispatchers.computation)
|
||||||
|
|
||||||
|
suspend fun updateVisibleRange(visibleRange: IntRange) {
|
||||||
|
roomList.updateVisibleRange(visibleRange)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
||||||
val filter = if (searchQuery.isBlank()) {
|
val filter = if (searchQuery.isBlank()) {
|
||||||
RoomListFilter.all()
|
RoomListFilter.all()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -51,6 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
|
|
@ -100,6 +102,11 @@ fun RoomSelectView(
|
||||||
onBack = { onBackButton(state) }
|
onBack = { onBackButton(state) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
|
state.eventSink(RoomSelectEvents.UpdateVisibleRange(visibleRange))
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|
@ -138,7 +145,7 @@ fun RoomSelectView(
|
||||||
resultState = state.resultState,
|
resultState = state.resultState,
|
||||||
showBackButton = false,
|
showBackButton = false,
|
||||||
) { summaries ->
|
) { summaries ->
|
||||||
LazyColumn {
|
LazyColumn(state = lazyListState) {
|
||||||
item {
|
item {
|
||||||
SelectedRoomsHelper(
|
SelectedRoomsHelper(
|
||||||
// TODO state.isForwarding
|
// TODO state.isForwarding
|
||||||
|
|
@ -170,7 +177,7 @@ fun RoomSelectView(
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
if (state.resultState is SearchBarResultState.Results) {
|
if (state.resultState is SearchBarResultState.Results) {
|
||||||
LazyColumn {
|
LazyColumn(state = lazyListState) {
|
||||||
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
||||||
Column {
|
Column {
|
||||||
RoomSummaryView(
|
RoomSummaryView(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue