RoomList : rework how search is done to prepare for later filtering
This commit is contained in:
@@ -20,7 +20,6 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface RoomListEvents {
|
||||
data class UpdateFilter(val newFilter: String) : RoomListEvents
|
||||
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
|
||||
data object DismissRequestVerificationPrompt : RoomListEvents
|
||||
data object DismissRecoveryKeyPrompt : RoomListEvents
|
||||
|
||||
@@ -37,6 +37,8 @@ import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
@@ -76,6 +78,7 @@ class RoomListPresenter @Inject constructor(
|
||||
private val encryptionService: EncryptionService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val indicatorService: IndicatorService,
|
||||
private val searchPresenter: RoomListSearchPresenter,
|
||||
private val migrationScreenPresenter: MigrationScreenPresenter,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
) : Presenter<RoomListState> {
|
||||
@@ -89,9 +92,8 @@ class RoomListPresenter @Inject constructor(
|
||||
val roomList by produceState(initialValue = AsyncData.Loading()) {
|
||||
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
||||
}
|
||||
val filteredRoomList by roomListDataSource.filteredRooms.collectAsState()
|
||||
val filter by roomListDataSource.filter.collectAsState()
|
||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
||||
val searchState = searchPresenter.present()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
roomListDataSource.launchIn(this)
|
||||
@@ -122,21 +124,14 @@ class RoomListPresenter @Inject constructor(
|
||||
// Avatar indicator
|
||||
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
||||
|
||||
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
||||
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
||||
|
||||
fun handleEvents(event: RoomListEvents) {
|
||||
when (event) {
|
||||
is RoomListEvents.UpdateFilter -> roomListDataSource.updateFilter(event.newFilter)
|
||||
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
|
||||
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true
|
||||
RoomListEvents.DismissRecoveryKeyPrompt -> recoveryKeyPromptDismissed = true
|
||||
RoomListEvents.ToggleSearchResults -> {
|
||||
if (displaySearchResults) {
|
||||
roomListDataSource.updateFilter("")
|
||||
}
|
||||
displaySearchResults = !displaySearchResults
|
||||
}
|
||||
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||
is RoomListEvents.ShowContextMenu -> {
|
||||
coroutineScope.showContextMenu(event, contextMenu)
|
||||
}
|
||||
@@ -175,16 +170,14 @@ class RoomListPresenter @Inject constructor(
|
||||
matrixUser = matrixUser.value,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
roomList = roomList,
|
||||
filter = filter,
|
||||
filteredRoomList = filteredRoomList,
|
||||
displayVerificationPrompt = displayVerificationPrompt,
|
||||
displayRecoveryKeyPrompt = displayRecoveryKeyPrompt,
|
||||
snackbarMessage = snackbarMessage,
|
||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
invitesState = inviteStateDataSource.inviteState(),
|
||||
displaySearchResults = displaySearchResults,
|
||||
contextMenu = contextMenu.value,
|
||||
leaveRoomState = leaveRoomState,
|
||||
searchState = searchState,
|
||||
displayMigrationStatus = isMigrating,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -30,16 +31,14 @@ data class RoomListState(
|
||||
val matrixUser: MatrixUser?,
|
||||
val showAvatarIndicator: Boolean,
|
||||
val roomList: AsyncData<ImmutableList<RoomListRoomSummary>>,
|
||||
val filter: String?,
|
||||
val filteredRoomList: ImmutableList<RoomListRoomSummary>,
|
||||
val displayVerificationPrompt: Boolean,
|
||||
val displayRecoveryKeyPrompt: Boolean,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val invitesState: InvitesState,
|
||||
val displaySearchResults: Boolean,
|
||||
val contextMenu: ContextMenu,
|
||||
val leaveRoomState: LeaveRoomState,
|
||||
val searchState: RoomListSearchState,
|
||||
val displayMigrationStatus: Boolean,
|
||||
val eventSink: (RoomListEvents) -> Unit,
|
||||
) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
@@ -41,14 +42,13 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||
aRoomListState().copy(hasNetworkConnection = false),
|
||||
aRoomListState().copy(invitesState = InvitesState.SeenInvites),
|
||||
aRoomListState().copy(invitesState = InvitesState.NewInvites),
|
||||
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
||||
aRoomListState().copy(displaySearchResults = true),
|
||||
aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
||||
aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)),
|
||||
aRoomListState().copy(displayRecoveryKeyPrompt = true),
|
||||
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
|
||||
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
|
||||
aRoomListState().copy(matrixUser = null, displayMigrationStatus = true),
|
||||
aRoomListState().copy(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,16 +56,14 @@ internal fun aRoomListState() = RoomListState(
|
||||
matrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
||||
showAvatarIndicator = false,
|
||||
roomList = AsyncData.Success(aRoomListRoomSummaryList()),
|
||||
filter = "filter",
|
||||
filteredRoomList = aRoomListRoomSummaryList(),
|
||||
hasNetworkConnection = true,
|
||||
snackbarMessage = null,
|
||||
displayVerificationPrompt = false,
|
||||
displayRecoveryKeyPrompt = false,
|
||||
invitesState = InvitesState.NoInvites,
|
||||
displaySearchResults = false,
|
||||
contextMenu = RoomListState.ContextMenu.Hidden,
|
||||
leaveRoomState = aLeaveRoomState(),
|
||||
searchState = aRoomListSearchState(),
|
||||
displayMigrationStatus = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchView
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -118,8 +118,8 @@ fun RoomListView(
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
)
|
||||
// This overlaid view will only be visible when state.displaySearchResults is true
|
||||
RoomListSearchResultView(
|
||||
state = state,
|
||||
RoomListSearchView(
|
||||
state = state.searchState,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
modifier = Modifier
|
||||
@@ -207,8 +207,7 @@ private fun RoomListContent(
|
||||
RoomListTopBar(
|
||||
matrixUser = state.matrixUser,
|
||||
showAvatarIndicator = state.showAvatarIndicator,
|
||||
areSearchResultsDisplayed = state.displaySearchResults,
|
||||
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||
areSearchResultsDisplayed = state.searchState.isSearchActive,
|
||||
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
onOpenSettings = onOpenSettings,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package io.element.android.features.roomlist.impl.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -87,7 +86,6 @@ fun RoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
showAvatarIndicator: Boolean,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
onFilterChanged: (String) -> Unit,
|
||||
onToggleSearch: () -> Unit,
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
@@ -95,15 +93,6 @@ fun RoomListTopBar(
|
||||
displayMenuItems: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun closeFilter() {
|
||||
onFilterChanged("")
|
||||
}
|
||||
|
||||
BackHandler(enabled = areSearchResultsDisplayed) {
|
||||
closeFilter()
|
||||
onToggleSearch()
|
||||
}
|
||||
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = matrixUser,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
|
||||
@@ -25,15 +25,11 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -54,9 +50,7 @@ class RoomListDataSource @Inject constructor(
|
||||
observeNotificationSettings()
|
||||
}
|
||||
|
||||
private val _filter = MutableStateFlow("")
|
||||
private val _allRooms = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
|
||||
private val _filteredRooms = MutableStateFlow<ImmutableList<RoomListRoomSummary>>(persistentListOf())
|
||||
|
||||
private val lock = Mutex()
|
||||
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
||||
@@ -72,29 +66,9 @@ class RoomListDataSource @Inject constructor(
|
||||
replaceWith(roomSummaries)
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
combine(
|
||||
_filter,
|
||||
_allRooms
|
||||
) { filterValue, allRoomsValue ->
|
||||
when {
|
||||
filterValue.isEmpty() -> emptyList()
|
||||
else -> allRoomsValue.filter { it.name.contains(filterValue, ignoreCase = true) }
|
||||
}.toImmutableList()
|
||||
}
|
||||
.onEach {
|
||||
_filteredRooms.value = it
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
fun updateFilter(filterValue: String) {
|
||||
_filter.value = filterValue
|
||||
}
|
||||
|
||||
val filter: StateFlow<String> = _filter
|
||||
val allRooms: SharedFlow<ImmutableList<RoomListRoomSummary>> = _allRooms
|
||||
val filteredRooms: StateFlow<ImmutableList<RoomListRoomSummary>> = _filteredRooms
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeNotificationSettings() {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl.search
|
||||
|
||||
sealed interface RoomListSearchEvents {
|
||||
data object ToggleSearchVisibility : RoomListSearchEvents
|
||||
data class QueryChanged(val query: String) : RoomListSearchEvents
|
||||
data object ClearQuery : RoomListSearchEvents
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl.search
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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.RoomSummary
|
||||
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val PAGE_SIZE = 50
|
||||
|
||||
class RoomListSearchPresenter @Inject constructor(
|
||||
private val roomListService: RoomListService,
|
||||
private val roomSummaryFactory: RoomListRoomSummaryFactory,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : Presenter<RoomListSearchState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomListSearchState {
|
||||
|
||||
var isSearchActive by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var searchQuery by rememberSaveable {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val roomList = remember {
|
||||
roomListService.createRoomList(
|
||||
coroutineScope = coroutineScope,
|
||||
pageSize = PAGE_SIZE,
|
||||
initialFilter = RoomListFilter.all(RoomListFilter.None),
|
||||
source = RoomList.Source.All,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
roomList.loadAllIncrementally(this)
|
||||
}
|
||||
LaunchedEffect(key1 = searchQuery) {
|
||||
val filter = if (searchQuery.isBlank()) {
|
||||
RoomListFilter.all(RoomListFilter.None)
|
||||
} else {
|
||||
RoomListFilter.all(RoomListFilter.NonLeft, RoomListFilter.NormalizedMatchRoomName(searchQuery))
|
||||
}
|
||||
roomList.updateFilter(filter)
|
||||
}
|
||||
|
||||
fun handleEvents(event: RoomListSearchEvents) {
|
||||
when (event) {
|
||||
RoomListSearchEvents.ClearQuery -> {
|
||||
searchQuery = ""
|
||||
}
|
||||
is RoomListSearchEvents.QueryChanged -> {
|
||||
searchQuery = event.query
|
||||
}
|
||||
RoomListSearchEvents.ToggleSearchVisibility -> {
|
||||
isSearchActive = !isSearchActive
|
||||
searchQuery = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchResults by roomList
|
||||
.rememberMappedSummaries()
|
||||
.collectAsState(initial = persistentListOf())
|
||||
|
||||
return RoomListSearchState(
|
||||
isSearchActive = isSearchActive,
|
||||
query = searchQuery,
|
||||
results = searchResults,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomList.rememberMappedSummaries() = remember {
|
||||
summaries
|
||||
.map { roomSummaries ->
|
||||
roomSummaries
|
||||
.filterIsInstance<RoomSummary.Filled>()
|
||||
.map(roomSummaryFactory::create)
|
||||
.toPersistentList()
|
||||
}
|
||||
.flowOn(coroutineDispatchers.computation)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl.search
|
||||
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomListSearchState(
|
||||
val isSearchActive: Boolean,
|
||||
val query: String,
|
||||
val results: ImmutableList<RoomListRoomSummary>,
|
||||
val eventSink: (RoomListSearchEvents) -> Unit
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl.search
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomlist.impl.aRoomListRoomSummaryList
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
class RoomListSearchStateProvider : PreviewParameterProvider<RoomListSearchState> {
|
||||
override val values: Sequence<RoomListSearchState>
|
||||
get() = sequenceOf(
|
||||
aRoomListSearchState(),
|
||||
aRoomListSearchState(
|
||||
isSearchActive = true,
|
||||
query = "Test",
|
||||
results = aRoomListRoomSummaryList()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aRoomListSearchState(
|
||||
isSearchActive: Boolean = false,
|
||||
query: String = "",
|
||||
results: ImmutableList<RoomListRoomSummary> = persistentListOf(),
|
||||
) = RoomListSearchState(
|
||||
isSearchActive = isSearchActive,
|
||||
query = query,
|
||||
results = results,
|
||||
eventSink = { },
|
||||
)
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.features.roomlist.impl.search
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -25,32 +26,23 @@ 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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||
import io.element.android.features.roomlist.impl.RoomListState
|
||||
import io.element.android.features.roomlist.impl.aRoomListState
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.contentType
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
@@ -68,26 +60,30 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
internal fun RoomListSearchResultView(
|
||||
state: RoomListState,
|
||||
internal fun RoomListSearchView(
|
||||
state: RoomListSearchState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler(enabled = state.isSearchActive) {
|
||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.displaySearchResults,
|
||||
visible = state.isSearchActive,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.applyIf(state.displaySearchResults, ifTrue = {
|
||||
.applyIf(state.isSearchActive, ifTrue = {
|
||||
// Disable input interaction to underlying views
|
||||
pointerInput(Unit) {}
|
||||
})
|
||||
) {
|
||||
if (state.displaySearchResults) {
|
||||
RoomListSearchResultContent(
|
||||
if (state.isSearchActive) {
|
||||
RoomListSearchContent(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = onRoomLongClicked,
|
||||
@@ -99,15 +95,15 @@ internal fun RoomListSearchResultView(
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun RoomListSearchResultContent(
|
||||
state: RoomListState,
|
||||
private fun RoomListSearchContent(
|
||||
state: RoomListSearchState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
) {
|
||||
val borderColor = MaterialTheme.colorScheme.tertiary
|
||||
val strokeWidth = 1.dp
|
||||
fun onBackButtonPressed() {
|
||||
state.eventSink(RoomListEvents.ToggleSearchResults)
|
||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||
}
|
||||
|
||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||
@@ -126,7 +122,7 @@ private fun RoomListSearchResultContent(
|
||||
},
|
||||
navigationIcon = { BackButton(onClick = ::onBackButtonPressed) },
|
||||
title = {
|
||||
val filter = state.filter.orEmpty()
|
||||
val filter = state.query
|
||||
val focusRequester = FocusRequester()
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
@@ -134,7 +130,7 @@ private fun RoomListSearchResultContent(
|
||||
.focusRequester(focusRequester),
|
||||
value = filter,
|
||||
singleLine = true,
|
||||
onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||
onValueChange = { state.eventSink(RoomListSearchEvents.QueryChanged(it)) },
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
@@ -147,7 +143,7 @@ private fun RoomListSearchResultContent(
|
||||
trailingIcon = {
|
||||
if (filter.isNotEmpty()) {
|
||||
IconButton(onClick = {
|
||||
state.eventSink(RoomListEvents.UpdateFilter(""))
|
||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
@@ -158,8 +154,8 @@ private fun RoomListSearchResultContent(
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(state.displaySearchResults) {
|
||||
if (state.displaySearchResults) {
|
||||
LaunchedEffect(state.isSearchActive) {
|
||||
if (state.isSearchActive) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
@@ -168,39 +164,16 @@ private fun RoomListSearchResultContent(
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
val lazyListState = rememberLazyListState()
|
||||
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 nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override suspend fun onPostFling(
|
||||
consumed: Velocity,
|
||||
available: Velocity
|
||||
): Velocity {
|
||||
state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
|
||||
return super.onPostFling(consumed, available)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = lazyListState,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
items(
|
||||
items = state.filteredRoomList,
|
||||
items = state.results,
|
||||
contentType = { room -> room.contentType() },
|
||||
) { room ->
|
||||
RoomSummaryRow(
|
||||
@@ -216,9 +189,9 @@ private fun RoomListSearchResultContent(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContentPreview() = ElementPreview {
|
||||
RoomListSearchResultContent(
|
||||
state = aRoomListState(),
|
||||
internal fun RoomListSearchResultContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview {
|
||||
RoomListSearchContent(
|
||||
state = state,
|
||||
onRoomClicked = {},
|
||||
onRoomLongClicked = {}
|
||||
)
|
||||
@@ -33,6 +33,8 @@ import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryF
|
||||
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
|
||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
||||
import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
||||
import io.element.android.features.roomlist.impl.search.createRoomListSearchPresenter
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
@@ -54,7 +56,6 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
@@ -145,24 +146,6 @@ class RoomListPresenterTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - should filter room with success`() = runTest {
|
||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val presenter = createRoomListPresenter(coroutineScope = scope)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val withUserState = awaitItem()
|
||||
assertThat(withUserState.filter).isEqualTo("")
|
||||
withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t"))
|
||||
val withFilterState = awaitItem()
|
||||
assertThat(withFilterState.filter).isEqualTo("t")
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - load 1 room with success`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
@@ -196,51 +179,7 @@ class RoomListPresenterTests {
|
||||
numberOfUnreadMessages = 2,
|
||||
)
|
||||
)
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - load 1 room with success and filter rooms`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val matrixClient = FakeMatrixClient(
|
||||
roomListService = roomListService
|
||||
)
|
||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
roomListService.postAllRooms(
|
||||
listOf(
|
||||
aRoomSummaryFilled(
|
||||
numUnreadMentions = 1,
|
||||
numUnreadMessages = 2,
|
||||
)
|
||||
)
|
||||
)
|
||||
skipItems(3)
|
||||
val loadedState = awaitItem()
|
||||
// Test filtering with result
|
||||
assertThat(loadedState.roomList.dataOrNull().orEmpty().size).isEqualTo(1)
|
||||
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
|
||||
skipItems(1)
|
||||
val withFilteredRoomState = awaitItem()
|
||||
assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
||||
assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
|
||||
assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
||||
assertThat(withFilteredRoomState.filteredRoomList.first()).isEqualTo(
|
||||
createRoomListRoomSummary(
|
||||
numberOfUnreadMentions = 1,
|
||||
numberOfUnreadMessages = 2,
|
||||
)
|
||||
)
|
||||
// Test filtering without result
|
||||
withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
|
||||
skipItems(1)
|
||||
val withNotFilteredRoomState = awaitItem()
|
||||
assertThat(withNotFilteredRoomState.filter).isEqualTo("tada")
|
||||
assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -572,7 +511,8 @@ class RoomListPresenterTests {
|
||||
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter(
|
||||
matrixClient = client,
|
||||
migrationScreenStore = InMemoryMigrationScreenStore(),
|
||||
)
|
||||
),
|
||||
searchPresenter: RoomListSearchPresenter = createRoomListSearchPresenter(roomListService = client.roomListService)
|
||||
) = RoomListPresenter(
|
||||
client = client,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
@@ -598,6 +538,7 @@ class RoomListPresenterTests {
|
||||
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
|
||||
),
|
||||
migrationScreenPresenter = migrationScreenPresenter,
|
||||
searchPresenter = searchPresenter,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl.search
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||
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.test.room.aRoomSummaryFilled
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class RoomListSearchPresenterTests {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createRoomListSearchPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.isSearchActive).isFalse()
|
||||
assertThat(state.query).isEmpty()
|
||||
assertThat(state.results).isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - toggle search visibility`() = runTest {
|
||||
val presenter = createRoomListSearchPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.isSearchActive).isFalse()
|
||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||
}
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.isSearchActive).isTrue()
|
||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||
}
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.isSearchActive).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - query search changes`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val presenter = createRoomListSearchPresenter(roomListService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().let { state ->
|
||||
assertThat(
|
||||
roomListService.allRooms.currentFilter.value
|
||||
).isEqualTo(
|
||||
RoomListFilter.all(
|
||||
RoomListFilter.None,
|
||||
)
|
||||
)
|
||||
state.eventSink(RoomListSearchEvents.QueryChanged("Search"))
|
||||
}
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.query).isEqualTo("Search")
|
||||
assertThat(
|
||||
roomListService.allRooms.currentFilter.value
|
||||
).isEqualTo(
|
||||
RoomListFilter.all(
|
||||
RoomListFilter.NonLeft,
|
||||
RoomListFilter.NormalizedMatchRoomName("Search")
|
||||
)
|
||||
)
|
||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||
}
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.query).isEmpty()
|
||||
assertThat(
|
||||
roomListService.allRooms.currentFilter.value
|
||||
).isEqualTo(
|
||||
RoomListFilter.all(
|
||||
RoomListFilter.None,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room list changes`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val presenter = createRoomListSearchPresenter(roomListService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.results).isEmpty()
|
||||
}
|
||||
roomListService.postAllRooms(
|
||||
listOf(
|
||||
RoomSummary.Empty("1"),
|
||||
aRoomSummaryFilled()
|
||||
)
|
||||
)
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.results).hasSize(1)
|
||||
}
|
||||
roomListService.postAllRooms(emptyList())
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.results).isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun TestScope.createRoomListSearchPresenter(
|
||||
roomListService: RoomListService = FakeRoomListService(),
|
||||
): RoomListSearchPresenter {
|
||||
return RoomListSearchPresenter(
|
||||
roomListService = roomListService,
|
||||
roomSummaryFactory = RoomListRoomSummaryFactory(
|
||||
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
|
||||
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||
),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
)
|
||||
}
|
||||
@@ -33,6 +33,11 @@ interface RoomList {
|
||||
data class Loaded(val numberOfRooms: Int) : LoadingState
|
||||
}
|
||||
|
||||
enum class Source {
|
||||
All,
|
||||
Invites,
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of room summaries as a flow.
|
||||
*/
|
||||
|
||||
@@ -18,38 +18,68 @@ package io.element.android.libraries.matrix.api.roomlist
|
||||
|
||||
sealed interface RoomListFilter {
|
||||
companion object {
|
||||
/**
|
||||
* Create a filter that matches all the given filters.
|
||||
*/
|
||||
fun all(vararg filters: RoomListFilter): RoomListFilter {
|
||||
return All(filters.toList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter that matches any of the given filters.
|
||||
*/
|
||||
fun any(vararg filters: RoomListFilter): RoomListFilter {
|
||||
return Any(filters.toList())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter that matches all the given filters.
|
||||
*/
|
||||
data class All(
|
||||
val filters: List<RoomListFilter>
|
||||
) : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches any of the given filters.
|
||||
*/
|
||||
data class Any(
|
||||
val filters: List<RoomListFilter>
|
||||
) : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches rooms that are not left.
|
||||
*/
|
||||
data object NonLeft : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches rooms that are unread.
|
||||
*/
|
||||
data object Unread : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches either Group or People rooms.
|
||||
*/
|
||||
sealed interface Category : RoomListFilter {
|
||||
data object Group : Category
|
||||
data object People : Category
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter that matches no room.
|
||||
*/
|
||||
data object None : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches rooms with a name using a normalized match.
|
||||
*/
|
||||
data class NormalizedMatchRoomName(
|
||||
val pattern: String
|
||||
) : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches rooms with a name using a fuzzy match.
|
||||
*/
|
||||
data class FuzzyMatchRoomName(
|
||||
val pattern: String
|
||||
) : RoomListFilter
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.libraries.matrix.api.roomlist
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
@@ -39,6 +40,20 @@ interface RoomListService {
|
||||
data object Hide : SyncIndicator
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a room list that can be used to load more rooms and filter them dynamically.
|
||||
* @param coroutineScope the scope to use for the room list. When the scope will be closed, the room list will be closed too.
|
||||
* @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.
|
||||
*/
|
||||
fun createRoomList(
|
||||
coroutineScope: CoroutineScope,
|
||||
pageSize: Int,
|
||||
initialFilter: RoomListFilter,
|
||||
source: RoomList.Source,
|
||||
): DynamicRoomList
|
||||
|
||||
/**
|
||||
* returns a [DynamicRoomList] object of all rooms we want to display.
|
||||
* This will exclude some rooms like the invites, or spaces.
|
||||
|
||||
@@ -199,8 +199,8 @@ class RustMatrixClient(
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
roomListFactory = RoomListFactory(
|
||||
innerRoomListService = innerRoomListService,
|
||||
coroutineScope = sessionCoroutineScope,
|
||||
dispatcher = sessionDispatcher,
|
||||
defaultCoroutineScope = sessionCoroutineScope,
|
||||
defaultCoroutineContext = sessionDispatcher,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ 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.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -30,13 +29,15 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
|
||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||
|
||||
internal class RoomListFactory(
|
||||
private val innerRoomListService: InnerRoomListService,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val defaultCoroutineScope: CoroutineScope,
|
||||
private val defaultCoroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
) {
|
||||
/**
|
||||
@@ -44,18 +45,21 @@ internal class RoomListFactory(
|
||||
*/
|
||||
fun createRoomList(
|
||||
pageSize: Int,
|
||||
coroutineScope: CoroutineScope = defaultCoroutineScope,
|
||||
coroutineContext: CoroutineContext = defaultCoroutineContext,
|
||||
initialFilter: RoomListFilter = RoomListFilter.all(),
|
||||
innerProvider: suspend () -> InnerRoomList
|
||||
): DynamicRoomList {
|
||||
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
|
||||
val summariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
|
||||
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory)
|
||||
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryDetailsFactory)
|
||||
// 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
|
||||
coroutineScope.launch(dispatcher) {
|
||||
|
||||
coroutineScope.launch(coroutineContext) {
|
||||
innerRoomList = innerProvider()
|
||||
innerRoomList?.let { innerRoomList ->
|
||||
innerRoomList.entriesFlow(
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package io.element.android.libraries.matrix.impl.roomlist
|
||||
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -28,11 +27,12 @@ import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class RoomSummaryListProcessor(
|
||||
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
|
||||
private val roomListService: RoomListServiceInterface,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val coroutineContext: CoroutineContext,
|
||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
) {
|
||||
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
|
||||
@@ -130,7 +130,7 @@ class RoomSummaryListProcessor(
|
||||
return builtRoomSummary
|
||||
}
|
||||
|
||||
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(dispatcher) {
|
||||
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
|
||||
mutex.withLock {
|
||||
val mutableRoomSummaries = roomSummaries.value.toMutableList()
|
||||
block(mutableRoomSummaries)
|
||||
|
||||
@@ -42,8 +42,26 @@ private const val DEFAULT_PAGE_SIZE = 20
|
||||
internal class RustRoomListService(
|
||||
private val innerRoomListService: InnerRustRoomListService,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
roomListFactory: RoomListFactory,
|
||||
private val roomListFactory: RoomListFactory,
|
||||
) : RoomListService {
|
||||
override fun createRoomList(
|
||||
coroutineScope: CoroutineScope,
|
||||
pageSize: Int,
|
||||
initialFilter: RoomListFilter,
|
||||
source: RoomList.Source
|
||||
): DynamicRoomList {
|
||||
return roomListFactory.createRoomList(
|
||||
pageSize = pageSize,
|
||||
initialFilter = initialFilter,
|
||||
coroutineScope = coroutineScope,
|
||||
) {
|
||||
when (source) {
|
||||
RoomList.Source.All -> innerRoomListService.allRooms()
|
||||
RoomList.Source.Invites -> innerRoomListService.invites()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val allRooms: DynamicRoomList = roomListFactory.createRoomList(
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
initialFilter = RoomListFilter.all(RoomListFilter.NonLeft),
|
||||
|
||||
@@ -158,7 +158,7 @@ class RoomSummaryListProcessorTests {
|
||||
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
|
||||
summaries,
|
||||
fakeRoomListService,
|
||||
dispatcher = StandardTestDispatcher(testScheduler),
|
||||
coroutineContext = StandardTestDispatcher(testScheduler),
|
||||
roomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
)
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ fun aRoomSummaryFilled(
|
||||
)
|
||||
)
|
||||
|
||||
fun aRoomSummaryFilled(
|
||||
details: RoomSummaryDetails = aRoomSummaryDetails(),
|
||||
) = RoomSummary.Filled(details)
|
||||
|
||||
fun aRoomSummaryDetails(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String = A_ROOM_NAME,
|
||||
|
||||
@@ -21,6 +21,7 @@ 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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -59,13 +60,20 @@ class FakeRoomListService : RoomListService {
|
||||
var latestSlidingSyncRange: IntRange? = null
|
||||
private set
|
||||
|
||||
override val allRooms: DynamicRoomList = SimplePagedRoomList(
|
||||
override fun createRoomList(coroutineScope: CoroutineScope, pageSize: Int, initialFilter: RoomListFilter, source: RoomList.Source): DynamicRoomList {
|
||||
return when (source) {
|
||||
RoomList.Source.All -> allRooms
|
||||
RoomList.Source.Invites -> invites
|
||||
}
|
||||
}
|
||||
|
||||
override val allRooms = SimplePagedRoomList(
|
||||
allRoomSummariesFlow,
|
||||
allRoomsLoadingStateFlow,
|
||||
MutableStateFlow(RoomListFilter.all())
|
||||
)
|
||||
|
||||
override val invites: RoomList = SimplePagedRoomList(
|
||||
override val invites = SimplePagedRoomList(
|
||||
inviteRoomSummariesFlow,
|
||||
inviteRoomsLoadingStateFlow,
|
||||
MutableStateFlow(RoomListFilter.all())
|
||||
|
||||
@@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
|
||||
data class SimplePagedRoomList(
|
||||
override val summaries: StateFlow<List<RoomSummary>>,
|
||||
override val summaries: MutableStateFlow<List<RoomSummary>>,
|
||||
override val loadingState: StateFlow<RoomList.LoadingState>,
|
||||
override val currentFilter: MutableStateFlow<RoomListFilter>
|
||||
) : DynamicRoomList {
|
||||
|
||||
Reference in New Issue
Block a user