Refactor room list filtering to use Rust SDK
This commit is contained in:
@@ -24,9 +24,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
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.IconSource
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@@ -215,17 +214,8 @@ private fun RoomsViewList(
|
||||
lazyListState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val visibleRange by remember {
|
||||
derivedStateOf {
|
||||
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))
|
||||
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||
eventSink(RoomListEvent.UpdateVisibleRange(visibleRange))
|
||||
}
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
@@ -237,7 +227,7 @@ private fun RoomsViewList(
|
||||
item {
|
||||
SetUpRecoveryKeyBanner(
|
||||
onContinueClick = onSetUpRecoveryClick,
|
||||
onDismissClick = { updatedEventSink(RoomListEvent.DismissBanner) },
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -245,7 +235,7 @@ private fun RoomsViewList(
|
||||
item {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClick = onConfirmRecoveryKeyClick,
|
||||
onDismissClick = { updatedEventSink(RoomListEvent.DismissBanner) },
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -260,7 +250,7 @@ private fun RoomsViewList(
|
||||
} else if (state.showNewNotificationSoundBanner) {
|
||||
item {
|
||||
NewNotificationSoundBanner(
|
||||
onDismissClick = { updatedEventSink(RoomListEvent.DismissNewNotificationSoundBanner) },
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissNewNotificationSoundBanner) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,33 +9,48 @@
|
||||
package io.element.android.features.home.impl.datasource
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
||||
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
||||
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
||||
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.matrix.api.core.RoomId
|
||||
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.RoomSummary
|
||||
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
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.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.IllegalStateException
|
||||
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
|
||||
@SingleIn(SessionScope::class)
|
||||
class RoomListDataSource(
|
||||
private val roomListService: RoomListService,
|
||||
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
|
||||
@@ -51,7 +66,12 @@ class RoomListDataSource(
|
||||
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 diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
||||
@@ -59,22 +79,49 @@ class RoomListDataSource(
|
||||
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) {
|
||||
roomListService
|
||||
.allRooms
|
||||
.filteredSummaries
|
||||
roomList
|
||||
.summaries
|
||||
.onEach { roomSummaries ->
|
||||
replaceWith(roomSummaries)
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
|
||||
roomListService.subscribeToVisibleRooms(roomIds)
|
||||
suspend fun updateFilter(filter: RoomListFilter) {
|
||||
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)
|
||||
@@ -82,7 +129,7 @@ class RoomListDataSource(
|
||||
notificationSettingsService.notificationSettingsChangeFlow
|
||||
.debounce(0.5.seconds)
|
||||
.onEach {
|
||||
roomListService.allRooms.rebuildSummaries()
|
||||
roomList.rebuildSummaries()
|
||||
}
|
||||
.launchIn(sessionCoroutineScope)
|
||||
}
|
||||
@@ -108,6 +155,7 @@ class RoomListDataSource(
|
||||
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
|
||||
// Used to detect duplicates in the room list summaries - see comment below
|
||||
data class CacheResult(val index: Int, val fromCache: Boolean)
|
||||
|
||||
val cachingResults = mutableMapOf<RoomId, MutableList<CacheResult>>()
|
||||
|
||||
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
|
||||
@@ -144,14 +192,14 @@ class RoomListDataSource(
|
||||
analyticsService.trackError(
|
||||
IllegalStateException(
|
||||
"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
|
||||
_allRooms.emit(roomListRoomSummaries.distinctBy { it.roomId }.toImmutableList())
|
||||
_roomSummariesFlow.emit(roomListRoomSummaries.distinctBy { it.roomId }.toImmutableList())
|
||||
} else {
|
||||
_allRooms.emit(roomListRoomSummaries.toImmutableList())
|
||||
_roomSummariesFlow.emit(roomListRoomSummaries.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +211,7 @@ class RoomListDataSource(
|
||||
|
||||
private suspend fun rebuildAllRoomSummaries() {
|
||||
lock.withLock {
|
||||
roomListService.allRooms.filteredSummaries.replayCache.firstOrNull()?.let { roomSummaries ->
|
||||
roomList.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
|
||||
buildAndEmitAllRooms(roomSummaries, useCache = false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,19 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
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.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
||||
|
||||
@Inject
|
||||
class RoomListFiltersPresenter(
|
||||
private val roomListService: RoomListService,
|
||||
private val roomListDataSource: RoomListDataSource,
|
||||
private val filterSelectionStrategy: FilterSelectionStrategy,
|
||||
) : Presenter<RoomListFiltersState> {
|
||||
private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
|
||||
@@ -56,9 +59,9 @@ class RoomListFiltersPresenter(
|
||||
}
|
||||
}
|
||||
}
|
||||
.collect { filters ->
|
||||
.collectLatest { 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.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -68,9 +66,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val EXTENDED_RANGE_SIZE = 40
|
||||
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
|
||||
|
||||
@Inject
|
||||
class RoomListPresenter(
|
||||
private val client: MatrixClient,
|
||||
@@ -119,7 +114,7 @@ class RoomListPresenter(
|
||||
fun handleEvent(event: RoomListEvent) {
|
||||
when (event) {
|
||||
is RoomListEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||
updateVisibleRange(event.range)
|
||||
roomListDataSource.updateVisibleRange(event.range)
|
||||
}
|
||||
RoomListEvent.DismissRequestVerificationPrompt -> securityBannerDismissed = true
|
||||
RoomListEvent.DismissBanner -> securityBannerDismissed = true
|
||||
@@ -217,7 +212,7 @@ class RoomListPresenter(
|
||||
showNewNotificationSoundBanner: Boolean,
|
||||
): RoomListContentState {
|
||||
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 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.RoomListFilter
|
||||
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.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -42,12 +42,11 @@ class RoomListSearchDataSource(
|
||||
|
||||
private val roomList = roomListService.createRoomList(
|
||||
pageSize = PAGE_SIZE,
|
||||
initialFilter = RoomListFilter.None,
|
||||
source = RoomList.Source.All,
|
||||
coroutineScope = coroutineScope
|
||||
)
|
||||
|
||||
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.filteredSummaries
|
||||
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.summaries
|
||||
.map { roomSummaries ->
|
||||
roomSummaries
|
||||
.map(roomSummaryFactory::create)
|
||||
@@ -55,12 +54,8 @@ class RoomListSearchDataSource(
|
||||
}
|
||||
.flowOn(coroutineDispatchers.computation)
|
||||
|
||||
suspend fun setIsActive(isActive: Boolean) = coroutineScope {
|
||||
if (isActive) {
|
||||
roomList.loadAllIncrementally(this)
|
||||
} else {
|
||||
roomList.reset()
|
||||
}
|
||||
suspend fun updateVisibleRange(visibleRange: IntRange) {
|
||||
roomList.updateVisibleRange(visibleRange)
|
||||
}
|
||||
|
||||
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
||||
@@ -71,4 +66,5 @@ class RoomListSearchDataSource(
|
||||
}
|
||||
roomList.updateFilter(filter)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ package io.element.android.features.home.impl.search
|
||||
sealed interface RoomListSearchEvent {
|
||||
data object ToggleSearchVisibility : 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 io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
class RoomListSearchPresenter(
|
||||
@@ -37,10 +38,6 @@ class RoomListSearchPresenter(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
||||
|
||||
LaunchedEffect(isSearchActive) {
|
||||
dataSource.setIsActive(isSearchActive)
|
||||
}
|
||||
|
||||
LaunchedEffect(searchQuery.text) {
|
||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||
}
|
||||
@@ -54,6 +51,9 @@ class RoomListSearchPresenter(
|
||||
isSearchActive = !isSearchActive
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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.Scaffold
|
||||
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.ui.strings.CommonStrings
|
||||
|
||||
@@ -154,7 +156,12 @@ private fun RoomListSearchContent(
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||
state.eventSink(RoomListSearchEvent.UpdateVisibleRange(visibleRange))
|
||||
}
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
items(
|
||||
|
||||
@@ -42,7 +42,7 @@ class RoomListDataSourceTest {
|
||||
dateTimeObserver = dateTimeObserver,
|
||||
)
|
||||
|
||||
roomListDataSource.allRooms.test {
|
||||
roomListDataSource.roomSummariesFlow.test {
|
||||
// Observe room list items changes
|
||||
roomListDataSource.launchIn(backgroundScope)
|
||||
// Get the initial room list
|
||||
@@ -75,7 +75,7 @@ class RoomListDataSourceTest {
|
||||
),
|
||||
dateTimeObserver = dateTimeObserver,
|
||||
)
|
||||
roomListDataSource.allRooms.test {
|
||||
roomListDataSource.roomSummariesFlow.test {
|
||||
// Observe room list items changes
|
||||
roomListDataSource.launchIn(backgroundScope)
|
||||
// Get the initial room list
|
||||
|
||||
@@ -9,12 +9,26 @@
|
||||
package io.element.android.features.home.impl.filters
|
||||
|
||||
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.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.test.notificationsettings.FakeNotificationSettingsService
|
||||
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.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 org.junit.Test
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
||||
@@ -39,13 +53,13 @@ class RoomListFiltersPresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun `present - toggle rooms filter`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val presenter = createRoomListFiltersPresenter(roomListService)
|
||||
presenter.test {
|
||||
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
||||
awaitLastSequentialItem().let { state ->
|
||||
|
||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||
assertThat(state.filterSelectionStates).containsExactly(
|
||||
filterSelectionState(RoomListFilter.Rooms, true),
|
||||
@@ -56,12 +70,9 @@ class RoomListFiltersPresenterTest {
|
||||
assertThat(state.selectedFilters()).containsExactly(
|
||||
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))
|
||||
}
|
||||
advanceUntilIdle()
|
||||
awaitLastSequentialItem().let { state ->
|
||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||
assertThat(state.filterSelectionStates).containsExactly(
|
||||
@@ -72,13 +83,12 @@ class RoomListFiltersPresenterTest {
|
||||
filterSelectionState(RoomListFilter.Invites, false),
|
||||
).inOrder()
|
||||
assertThat(state.selectedFilters()).isEmpty()
|
||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
||||
assertThat(roomListCurrentFilter.filters).isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun `present - clear filters event`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val presenter = createRoomListFiltersPresenter(roomListService)
|
||||
@@ -88,6 +98,7 @@ class RoomListFiltersPresenterTest {
|
||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||
state.eventSink.invoke(RoomListFiltersEvent.ClearSelectedFilters)
|
||||
}
|
||||
advanceUntilIdle()
|
||||
awaitLastSequentialItem().let { state ->
|
||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||
}
|
||||
@@ -100,11 +111,25 @@ private fun filterSelectionState(filter: RoomListFilter, selected: Boolean) = Fi
|
||||
isSelected = selected,
|
||||
)
|
||||
|
||||
private fun createRoomListFiltersPresenter(
|
||||
private fun TestScope.createRoomListFiltersPresenter(
|
||||
roomListService: RoomListService = FakeRoomListService(),
|
||||
notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||
roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
||||
): 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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ sealed interface AddRoomToSpaceEvent {
|
||||
data object Save : AddRoomToSpaceEvent
|
||||
data object ResetSaveAction : AddRoomToSpaceEvent
|
||||
data object Dismiss : AddRoomToSpaceEvent
|
||||
data class UpdateSearchVisibleRange(val range: IntRange) : AddRoomToSpaceEvent
|
||||
}
|
||||
|
||||
@@ -58,9 +58,6 @@ class AddRoomToSpacePresenter(
|
||||
LaunchedEffect(searchQuery.text) {
|
||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||
}
|
||||
LaunchedEffect(isSearchActive) {
|
||||
dataSource.setIsActive(isSearchActive)
|
||||
}
|
||||
|
||||
val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf())
|
||||
|
||||
@@ -111,6 +108,9 @@ class AddRoomToSpacePresenter(
|
||||
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.RoomListFilter
|
||||
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.ui.model.SelectRoomInfo
|
||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -58,7 +57,6 @@ class AddRoomToSpaceSearchDataSource(
|
||||
|
||||
private val roomList = roomListService.createRoomList(
|
||||
pageSize = PAGE_SIZE,
|
||||
initialFilter = RoomListFilter.all(),
|
||||
source = RoomList.Source.All,
|
||||
coroutineScope = coroutineScope,
|
||||
)
|
||||
@@ -87,7 +85,7 @@ class AddRoomToSpaceSearchDataSource(
|
||||
}
|
||||
|
||||
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
||||
roomList.filteredSummaries,
|
||||
roomList.summaries,
|
||||
spaceChildrenFlow,
|
||||
addedRoomIdsFlow,
|
||||
) { roomSummaries, childIds, addedIds ->
|
||||
@@ -109,12 +107,8 @@ class AddRoomToSpaceSearchDataSource(
|
||||
.toImmutableList()
|
||||
}.flowOn(coroutineDispatchers.computation)
|
||||
|
||||
suspend fun setIsActive(isActive: Boolean) = coroutineScope {
|
||||
if (isActive) {
|
||||
roomList.loadAllIncrementally(this)
|
||||
} else {
|
||||
roomList.reset()
|
||||
}
|
||||
suspend fun updateVisibleRange(visibleRange: IntRange) {
|
||||
roomList.updateVisibleRange(visibleRange)
|
||||
}
|
||||
|
||||
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.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
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.TextButton
|
||||
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.model.SelectRoomInfo
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
@@ -121,6 +123,10 @@ fun AddRoomToSpaceView(
|
||||
}
|
||||
},
|
||||
) { rooms ->
|
||||
val lazyListState = rememberLazyListState()
|
||||
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||
state.eventSink(AddRoomToSpaceEvent.UpdateSearchVisibleRange(visibleRange))
|
||||
}
|
||||
LazyColumn {
|
||||
items(rooms, key = { it.roomId }) { roomInfo ->
|
||||
RoomListItem(
|
||||
|
||||
Reference in New Issue
Block a user