Merge pull request #2536 from element-hq/feature/fga/room_list_filter_iteration
Feature/fga/room list filter iteration
This commit is contained in:
commit
eba6db5ce2
126 changed files with 1240 additions and 561 deletions
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
open class RoomListContentStateProvider : PreviewParameterProvider<RoomListContentState> {
|
||||||
|
override val values: Sequence<RoomListContentState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aRoomsContentState(),
|
||||||
|
aRoomsContentState(summaries = persistentListOf()),
|
||||||
|
aSkeletonContentState(),
|
||||||
|
anEmptyContentState(),
|
||||||
|
aMigrationContentState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun aRoomsContentState(
|
||||||
|
invitesState: InvitesState = InvitesState.NoInvites,
|
||||||
|
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
||||||
|
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
||||||
|
) = RoomListContentState.Rooms(
|
||||||
|
invitesState = invitesState,
|
||||||
|
securityBannerState = securityBannerState,
|
||||||
|
summaries = summaries,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun aMigrationContentState() = RoomListContentState.Migration
|
||||||
|
|
||||||
|
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
|
||||||
|
|
||||||
|
internal fun anEmptyContentState(
|
||||||
|
invitesState: InvitesState = InvitesState.NoInvites,
|
||||||
|
) = RoomListContentState.Empty(invitesState)
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -26,6 +27,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
|
@ -38,7 +40,7 @@ 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.InviteStateDataSource
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
@ -52,6 +54,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
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.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
|
@ -60,6 +63,7 @@ import io.element.android.libraries.matrix.api.user.getCurrentUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
|
@ -85,7 +89,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val indicatorService: IndicatorService,
|
private val indicatorService: IndicatorService,
|
||||||
private val filtersPresenter: Presenter<RoomListFiltersState>,
|
private val filtersPresenter: Presenter<RoomListFiltersState>,
|
||||||
private val searchPresenter: Presenter<RoomListSearchState>,
|
private val searchPresenter: Presenter<RoomListSearchState>,
|
||||||
private val migrationScreenPresenter: MigrationScreenPresenter,
|
private val migrationScreenPresenter: Presenter<MigrationScreenState>,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
|
|
@ -100,11 +104,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
|
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
|
||||||
mutableStateOf(null)
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
val roomList by produceState(initialValue = AsyncData.Loading()) {
|
|
||||||
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
|
||||||
}
|
|
||||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
||||||
|
|
||||||
val filtersState = filtersPresenter.present()
|
val filtersState = filtersPresenter.present()
|
||||||
val searchState = searchPresenter.present()
|
val searchState = searchPresenter.present()
|
||||||
|
|
||||||
|
|
@ -113,28 +113,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
initialLoad(matrixUser)
|
initialLoad(matrixUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isMigrating = migrationScreenPresenter.present().isMigrating
|
|
||||||
|
|
||||||
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
|
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
|
||||||
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
|
|
||||||
val isLastDevice by encryptionService.isLastDevice.collectAsState()
|
|
||||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
|
||||||
val syncState by syncService.syncState.collectAsState()
|
|
||||||
val securityBannerState by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
when {
|
|
||||||
securityBannerDismissed -> SecurityBannerState.None
|
|
||||||
canVerifySession -> if (isLastDevice) {
|
|
||||||
SecurityBannerState.RecoveryKeyConfirmation
|
|
||||||
} else {
|
|
||||||
SecurityBannerState.SessionVerification
|
|
||||||
}
|
|
||||||
recoveryState == RecoveryState.INCOMPLETE &&
|
|
||||||
syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation
|
|
||||||
else -> SecurityBannerState.None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avatar indicator
|
// Avatar indicator
|
||||||
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
||||||
|
|
@ -162,19 +141,18 @@ class RoomListPresenter @Inject constructor(
|
||||||
|
|
||||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||||
|
|
||||||
|
val contentState = roomListContentState(securityBannerDismissed)
|
||||||
|
|
||||||
return RoomListState(
|
return RoomListState(
|
||||||
matrixUser = matrixUser.value,
|
matrixUser = matrixUser.value,
|
||||||
showAvatarIndicator = showAvatarIndicator,
|
showAvatarIndicator = showAvatarIndicator,
|
||||||
roomList = roomList,
|
|
||||||
securityBannerState = securityBannerState,
|
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
invitesState = inviteStateDataSource.inviteState(),
|
|
||||||
contextMenu = contextMenu.value,
|
contextMenu = contextMenu.value,
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
displayMigrationStatus = isMigrating,
|
contentState = contentState,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -183,6 +161,70 @@ class RoomListPresenter @Inject constructor(
|
||||||
matrixUser.value = client.getCurrentUser()
|
matrixUser.value = client.getCurrentUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun securityBannerState(
|
||||||
|
securityBannerDismissed: Boolean,
|
||||||
|
): State<SecurityBannerState> {
|
||||||
|
val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
|
||||||
|
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
|
||||||
|
val isLastDevice by encryptionService.isLastDevice.collectAsState()
|
||||||
|
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||||
|
val syncState by syncService.syncState.collectAsState()
|
||||||
|
return remember {
|
||||||
|
derivedStateOf {
|
||||||
|
when {
|
||||||
|
currentSecurityBannerDismissed -> SecurityBannerState.None
|
||||||
|
canVerifySession -> if (isLastDevice) {
|
||||||
|
SecurityBannerState.RecoveryKeyConfirmation
|
||||||
|
} else {
|
||||||
|
SecurityBannerState.SessionVerification
|
||||||
|
}
|
||||||
|
recoveryState == RecoveryState.INCOMPLETE &&
|
||||||
|
syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation
|
||||||
|
else -> SecurityBannerState.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun roomListContentState(
|
||||||
|
securityBannerDismissed: Boolean,
|
||||||
|
): RoomListContentState {
|
||||||
|
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
||||||
|
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
||||||
|
}
|
||||||
|
val loadingState by roomListDataSource.loadingState.collectAsState()
|
||||||
|
val showMigration = migrationScreenPresenter.present().isMigrating
|
||||||
|
val showEmpty by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val showSkeleton by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
showMigration -> RoomListContentState.Migration
|
||||||
|
showEmpty -> {
|
||||||
|
val invitesState = inviteStateDataSource.inviteState()
|
||||||
|
RoomListContentState.Empty(invitesState)
|
||||||
|
}
|
||||||
|
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
||||||
|
else -> {
|
||||||
|
val invitesState = inviteStateDataSource.inviteState()
|
||||||
|
val securityBannerState by securityBannerState(securityBannerDismissed)
|
||||||
|
RoomListContentState.Rooms(
|
||||||
|
invitesState = invitesState,
|
||||||
|
securityBannerState = securityBannerState,
|
||||||
|
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState<RoomListState.ContextMenu>) = launch {
|
private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState<RoomListState.ContextMenu>) = launch {
|
||||||
val initialState = RoomListState.ContextMenu.Shown(
|
val initialState = RoomListState.ContextMenu.Shown(
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
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.designsystem.utils.snackbar.SnackbarMessage
|
||||||
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.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -31,20 +30,17 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
data class RoomListState(
|
data class RoomListState(
|
||||||
val matrixUser: MatrixUser?,
|
val matrixUser: MatrixUser?,
|
||||||
val showAvatarIndicator: Boolean,
|
val showAvatarIndicator: Boolean,
|
||||||
val roomList: AsyncData<ImmutableList<RoomListRoomSummary>>,
|
|
||||||
val securityBannerState: SecurityBannerState,
|
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val invitesState: InvitesState,
|
|
||||||
val contextMenu: ContextMenu,
|
val contextMenu: ContextMenu,
|
||||||
val leaveRoomState: LeaveRoomState,
|
val leaveRoomState: LeaveRoomState,
|
||||||
val filtersState: RoomListFiltersState,
|
val filtersState: RoomListFiltersState,
|
||||||
val searchState: RoomListSearchState,
|
val searchState: RoomListSearchState,
|
||||||
val displayMigrationStatus: Boolean,
|
val contentState: RoomListContentState,
|
||||||
val eventSink: (RoomListEvents) -> Unit,
|
val eventSink: (RoomListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val displayFilters = filtersState.isFeatureEnabled && !displayMigrationStatus
|
val displayFilters = filtersState.isFeatureEnabled && contentState is RoomListContentState.Rooms
|
||||||
val displayEmptyState = roomList is AsyncData.Success && roomList.data.isEmpty()
|
val displayActions = contentState !is RoomListContentState.Migration
|
||||||
|
|
||||||
sealed interface ContextMenu {
|
sealed interface ContextMenu {
|
||||||
data object Hidden : ContextMenu
|
data object Hidden : ContextMenu
|
||||||
|
|
@ -70,3 +66,15 @@ enum class SecurityBannerState {
|
||||||
SessionVerification,
|
SessionVerification,
|
||||||
RecoveryKeyConfirmation,
|
RecoveryKeyConfirmation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface RoomListContentState {
|
||||||
|
data object Migration : RoomListContentState
|
||||||
|
data class Skeleton(val count: Int) : RoomListContentState
|
||||||
|
data class Empty(val invitesState: InvitesState) : RoomListContentState
|
||||||
|
data class Rooms(
|
||||||
|
val invitesState: InvitesState,
|
||||||
|
val securityBannerState: SecurityBannerState,
|
||||||
|
val summaries: ImmutableList<RoomListRoomSummary>,
|
||||||
|
) : RoomListContentState
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,12 @@ package io.element.android.features.roomlist.impl
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
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.model.aRoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
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.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
|
|
@ -43,15 +41,15 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState(),
|
aRoomListState(),
|
||||||
aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
||||||
aRoomListState(hasNetworkConnection = false),
|
aRoomListState(hasNetworkConnection = false),
|
||||||
aRoomListState(invitesState = InvitesState.SeenInvites),
|
aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.SeenInvites)),
|
||||||
aRoomListState(invitesState = InvitesState.NewInvites),
|
aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.NewInvites)),
|
||||||
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
||||||
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
|
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
|
||||||
aRoomListState(securityBannerState = SecurityBannerState.SessionVerification),
|
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification)),
|
||||||
aRoomListState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
|
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
||||||
aRoomListState(roomList = AsyncData.Success(persistentListOf())),
|
aRoomListState(contentState = anEmptyContentState()),
|
||||||
aRoomListState(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
|
aRoomListState(contentState = aSkeletonContentState()),
|
||||||
aRoomListState(matrixUser = null, displayMigrationStatus = true),
|
aRoomListState(matrixUser = null, contentState = aMigrationContentState()),
|
||||||
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||||
aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
|
aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
|
||||||
)
|
)
|
||||||
|
|
@ -60,30 +58,24 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
internal fun aRoomListState(
|
internal fun aRoomListState(
|
||||||
matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
||||||
showAvatarIndicator: Boolean = false,
|
showAvatarIndicator: Boolean = false,
|
||||||
roomList: AsyncData<ImmutableList<RoomListRoomSummary>> = AsyncData.Success(aRoomListRoomSummaryList()),
|
|
||||||
hasNetworkConnection: Boolean = true,
|
hasNetworkConnection: Boolean = true,
|
||||||
snackbarMessage: SnackbarMessage? = null,
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
|
||||||
invitesState: InvitesState = InvitesState.NoInvites,
|
|
||||||
contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden,
|
contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden,
|
||||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||||
searchState: RoomListSearchState = aRoomListSearchState(),
|
searchState: RoomListSearchState = aRoomListSearchState(),
|
||||||
filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false),
|
filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false),
|
||||||
displayMigrationStatus: Boolean = false,
|
contentState: RoomListContentState = aRoomsContentState(),
|
||||||
eventSink: (RoomListEvents) -> Unit = {}
|
eventSink: (RoomListEvents) -> Unit = {}
|
||||||
) = RoomListState(
|
) = RoomListState(
|
||||||
matrixUser = matrixUser,
|
matrixUser = matrixUser,
|
||||||
showAvatarIndicator = showAvatarIndicator,
|
showAvatarIndicator = showAvatarIndicator,
|
||||||
roomList = roomList,
|
|
||||||
hasNetworkConnection = hasNetworkConnection,
|
hasNetworkConnection = hasNetworkConnection,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
securityBannerState = securityBannerState,
|
|
||||||
invitesState = invitesState,
|
|
||||||
contextMenu = contextMenu,
|
contextMenu = contextMenu,
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
searchState = searchState,
|
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
displayMigrationStatus = displayMigrationStatus,
|
searchState = searchState,
|
||||||
|
contentState = contentState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,62 +17,36 @@
|
||||||
package io.element.android.features.roomlist.impl
|
package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
|
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
|
||||||
import io.element.android.features.roomlist.impl.components.ConfirmRecoveryKeyBanner
|
import io.element.android.features.roomlist.impl.components.RoomListContentView
|
||||||
import io.element.android.features.roomlist.impl.components.RequestVerificationHeader
|
|
||||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||||
import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
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.filters.RoomListFiltersView
|
|
||||||
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.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchView
|
import io.element.android.features.roomlist.impl.search.RoomListSearchView
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
|
||||||
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.Text
|
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
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
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListView(
|
fun RoomListView(
|
||||||
|
|
@ -108,7 +82,7 @@ fun RoomListView(
|
||||||
|
|
||||||
LeaveRoomView(state = state.leaveRoomState)
|
LeaveRoomView(state = state.leaveRoomState)
|
||||||
|
|
||||||
RoomListContent(
|
RoomListScaffold(
|
||||||
modifier = Modifier.padding(top = topPadding),
|
modifier = Modifier.padding(top = topPadding),
|
||||||
state = state,
|
state = state,
|
||||||
onVerifyClicked = onVerifyClicked,
|
onVerifyClicked = onVerifyClicked,
|
||||||
|
|
@ -135,43 +109,9 @@ fun RoomListView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun EmptyRoomListView(
|
|
||||||
onCreateRoomClicked: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_roomlist_empty_title),
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_roomlist_empty_message),
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Button(
|
|
||||||
text = stringResource(CommonStrings.action_start_chat),
|
|
||||||
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
|
|
||||||
onClick = onCreateRoomClicked,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomListContent(
|
private fun RoomListScaffold(
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
onVerifyClicked: () -> Unit,
|
onVerifyClicked: () -> Unit,
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
|
|
@ -188,111 +128,43 @@ private fun RoomListContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
val appBarState = rememberTopAppBarState()
|
val appBarState = rememberTopAppBarState()
|
||||||
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 scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
|
||||||
val nestedScrollConnection = remember {
|
|
||||||
object : NestedScrollConnection {
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
|
||||||
state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
|
|
||||||
return super.onPostFling(consumed, available)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
Column {
|
RoomListTopBar(
|
||||||
RoomListTopBar(
|
matrixUser = state.matrixUser,
|
||||||
matrixUser = state.matrixUser,
|
showAvatarIndicator = state.showAvatarIndicator,
|
||||||
showAvatarIndicator = state.showAvatarIndicator,
|
areSearchResultsDisplayed = state.searchState.isSearchActive,
|
||||||
areSearchResultsDisplayed = state.searchState.isSearchActive,
|
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
|
||||||
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
|
onMenuActionClicked = onMenuActionClicked,
|
||||||
onMenuActionClicked = onMenuActionClicked,
|
onOpenSettings = onOpenSettings,
|
||||||
onOpenSettings = onOpenSettings,
|
scrollBehavior = scrollBehavior,
|
||||||
scrollBehavior = scrollBehavior,
|
displayMenuItems = state.displayActions,
|
||||||
displayMenuItems = !state.displayMigrationStatus,
|
displayFilters = state.displayFilters,
|
||||||
)
|
filtersState = state.filtersState,
|
||||||
if (state.displayFilters) {
|
)
|
||||||
RoomListFiltersView(state = state.filtersState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
LazyColumn(
|
RoomListContentView(
|
||||||
|
contentState = state.contentState,
|
||||||
|
filtersState = state.filtersState,
|
||||||
|
eventSink = state.eventSink,
|
||||||
|
onVerifyClicked = onVerifyClicked,
|
||||||
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
|
onRoomClicked = ::onRoomClicked,
|
||||||
|
onRoomLongClicked = onRoomLongClicked,
|
||||||
|
onCreateRoomClicked = onCreateRoomClicked,
|
||||||
|
onInvitesClicked = onInvitesClicked,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
.nestedScroll(nestedScrollConnection),
|
)
|
||||||
state = lazyListState,
|
|
||||||
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
|
|
||||||
contentPadding = PaddingValues(bottom = 80.dp)
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
state.displayEmptyState -> Unit
|
|
||||||
state.securityBannerState == SecurityBannerState.SessionVerification -> {
|
|
||||||
item {
|
|
||||||
RequestVerificationHeader(
|
|
||||||
onVerifyClicked = onVerifyClicked,
|
|
||||||
onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.securityBannerState == SecurityBannerState.RecoveryKeyConfirmation -> {
|
|
||||||
item {
|
|
||||||
ConfirmRecoveryKeyBanner(
|
|
||||||
onContinueClicked = onConfirmRecoveryKeyClicked,
|
|
||||||
onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.invitesState != InvitesState.NoInvites) {
|
|
||||||
item {
|
|
||||||
InvitesEntryPointView(onInvitesClicked, state.invitesState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val roomList = state.roomList.dataOrNull().orEmpty()
|
|
||||||
// Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room
|
|
||||||
// is moved to the top of the list.
|
|
||||||
itemsIndexed(
|
|
||||||
items = roomList,
|
|
||||||
contentType = { _, room -> room.contentType() },
|
|
||||||
) { index, room ->
|
|
||||||
RoomSummaryRow(
|
|
||||||
room = room,
|
|
||||||
onClick = ::onRoomClicked,
|
|
||||||
onLongClick = onRoomLongClicked,
|
|
||||||
)
|
|
||||||
if (index != roomList.lastIndex) {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.displayEmptyState) {
|
|
||||||
if (state.filtersState.hasAnyFilterSelected) {
|
|
||||||
// TODO add empty state for filtered rooms
|
|
||||||
} else {
|
|
||||||
EmptyRoomListView(onCreateRoomClicked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MigrationScreenView(isMigrating = state.displayMigrationStatus)
|
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (!state.displayMigrationStatus) {
|
if (state.displayActions) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
// FIXME align on Design system theme
|
// FIXME align on Design system theme
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* 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.components
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.roomlist.impl.InvitesEntryPointView
|
||||||
|
import io.element.android.features.roomlist.impl.InvitesState
|
||||||
|
import io.element.android.features.roomlist.impl.R
|
||||||
|
import io.element.android.features.roomlist.impl.RoomListContentState
|
||||||
|
import io.element.android.features.roomlist.impl.RoomListContentStateProvider
|
||||||
|
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||||
|
import io.element.android.features.roomlist.impl.SecurityBannerState
|
||||||
|
import io.element.android.features.roomlist.impl.contentType
|
||||||
|
import io.element.android.features.roomlist.impl.filters.RoomListFilter
|
||||||
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersEmptyStateResources
|
||||||
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
|
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||||
|
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
|
||||||
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
|
||||||
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
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.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoomListContentView(
|
||||||
|
contentState: RoomListContentState,
|
||||||
|
filtersState: RoomListFiltersState,
|
||||||
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
|
onVerifyClicked: () -> Unit,
|
||||||
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
|
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
onCreateRoomClicked: () -> Unit,
|
||||||
|
onInvitesClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
when (contentState) {
|
||||||
|
is RoomListContentState.Migration -> {
|
||||||
|
MigrationScreenView(isMigrating = true)
|
||||||
|
}
|
||||||
|
is RoomListContentState.Skeleton -> {
|
||||||
|
SkeletonView(
|
||||||
|
count = contentState.count,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RoomListContentState.Empty -> {
|
||||||
|
EmptyView(
|
||||||
|
state = contentState,
|
||||||
|
onInvitesClicked = onInvitesClicked,
|
||||||
|
onCreateRoomClicked = onCreateRoomClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RoomListContentState.Rooms -> {
|
||||||
|
RoomsView(
|
||||||
|
state = contentState,
|
||||||
|
filtersState = filtersState,
|
||||||
|
eventSink = eventSink,
|
||||||
|
onVerifyClicked = onVerifyClicked,
|
||||||
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
|
onRoomClicked = onRoomClicked,
|
||||||
|
onRoomLongClicked = onRoomLongClicked,
|
||||||
|
onInvitesClicked = onInvitesClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SkeletonView(count: Int, modifier: Modifier = Modifier) {
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
repeat(count) { index ->
|
||||||
|
item {
|
||||||
|
RoomSummaryPlaceholderRow()
|
||||||
|
if (index != count - 1) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyView(
|
||||||
|
state: RoomListContentState.Empty,
|
||||||
|
onCreateRoomClicked: () -> Unit,
|
||||||
|
onInvitesClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
if (state.invitesState != InvitesState.NoInvites) {
|
||||||
|
InvitesEntryPointView(onInvitesClicked, state.invitesState)
|
||||||
|
}
|
||||||
|
EmptyScaffold(
|
||||||
|
title = R.string.screen_roomlist_empty_title,
|
||||||
|
subtitle = R.string.screen_roomlist_empty_message,
|
||||||
|
action = {
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_start_chat),
|
||||||
|
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
|
||||||
|
onClick = onCreateRoomClicked,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomsView(
|
||||||
|
state: RoomListContentState.Rooms,
|
||||||
|
filtersState: RoomListFiltersState,
|
||||||
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
|
onVerifyClicked: () -> Unit,
|
||||||
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
|
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
onInvitesClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
|
||||||
|
EmptyViewForFilterStates(
|
||||||
|
selectedFilters = filtersState.selectedFilters(),
|
||||||
|
modifier = modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RoomsViewList(
|
||||||
|
state = state,
|
||||||
|
eventSink = eventSink,
|
||||||
|
onVerifyClicked = onVerifyClicked,
|
||||||
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
|
onRoomClicked = onRoomClicked,
|
||||||
|
onRoomLongClicked = onRoomLongClicked,
|
||||||
|
onInvitesClicked = onInvitesClicked,
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomsViewList(
|
||||||
|
state: RoomListContentState.Rooms,
|
||||||
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
|
onVerifyClicked: () -> Unit,
|
||||||
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
|
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
onInvitesClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
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 {
|
||||||
|
eventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
|
||||||
|
return super.onPostFling(consumed, available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
modifier = modifier.nestedScroll(nestedScrollConnection),
|
||||||
|
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
|
||||||
|
contentPadding = PaddingValues(bottom = 80.dp)
|
||||||
|
) {
|
||||||
|
when (state.securityBannerState) {
|
||||||
|
SecurityBannerState.SessionVerification -> {
|
||||||
|
item {
|
||||||
|
RequestVerificationHeader(
|
||||||
|
onVerifyClicked = onVerifyClicked,
|
||||||
|
onDismissClicked = { eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||||
|
item {
|
||||||
|
ConfirmRecoveryKeyBanner(
|
||||||
|
onContinueClicked = onConfirmRecoveryKeyClicked,
|
||||||
|
onDismissClicked = { eventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.invitesState != InvitesState.NoInvites) {
|
||||||
|
item {
|
||||||
|
InvitesEntryPointView(onInvitesClicked, state.invitesState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room
|
||||||
|
// is moved to the top of the list.
|
||||||
|
itemsIndexed(
|
||||||
|
items = state.summaries,
|
||||||
|
contentType = { _, room -> room.contentType() },
|
||||||
|
) { index, room ->
|
||||||
|
RoomSummaryRow(
|
||||||
|
room = room,
|
||||||
|
onClick = onRoomClicked,
|
||||||
|
onLongClick = onRoomLongClicked,
|
||||||
|
)
|
||||||
|
if (index != state.summaries.lastIndex) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyViewForFilterStates(
|
||||||
|
selectedFilters: ImmutableList<RoomListFilter>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return
|
||||||
|
EmptyScaffold(
|
||||||
|
title = emptyStateResources.title,
|
||||||
|
subtitle = emptyStateResources.subtitle,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyScaffold(
|
||||||
|
@StringRes title: Int,
|
||||||
|
@StringRes subtitle: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
action: @Composable (ColumnScope.() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.padding(horizontal = 60.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(title),
|
||||||
|
style = ElementTheme.typography.fontHeadingMdBold,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(subtitle),
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
action?.invoke(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStateProvider::class) state: RoomListContentState) = ElementPreview {
|
||||||
|
RoomListContentView(
|
||||||
|
contentState = state,
|
||||||
|
filtersState = aRoomListFiltersState(
|
||||||
|
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
|
||||||
|
),
|
||||||
|
eventSink = {},
|
||||||
|
onVerifyClicked = { },
|
||||||
|
onConfirmRecoveryKeyClicked = { },
|
||||||
|
onRoomClicked = {},
|
||||||
|
onRoomLongClicked = {},
|
||||||
|
onCreateRoomClicked = { },
|
||||||
|
onInvitesClicked = { })
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.roomlist.impl.components
|
package io.element.android.features.roomlist.impl.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -40,7 +41,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
|
@ -52,8 +52,12 @@ import io.element.android.appconfig.RoomListConfig
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.roomlist.impl.R
|
import io.element.android.features.roomlist.impl.R
|
||||||
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersView
|
||||||
|
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.avatarBloom
|
import io.element.android.libraries.designsystem.components.avatarBloom
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -91,6 +95,8 @@ fun RoomListTopBar(
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
scrollBehavior: TopAppBarScrollBehavior,
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
displayMenuItems: Boolean,
|
displayMenuItems: Boolean,
|
||||||
|
displayFilters: Boolean,
|
||||||
|
filtersState: RoomListFiltersState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
DefaultRoomListTopBar(
|
DefaultRoomListTopBar(
|
||||||
|
|
@ -102,6 +108,8 @@ fun RoomListTopBar(
|
||||||
onMenuActionClicked = onMenuActionClicked,
|
onMenuActionClicked = onMenuActionClicked,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
displayMenuItems = displayMenuItems,
|
displayMenuItems = displayMenuItems,
|
||||||
|
displayFilters = displayFilters,
|
||||||
|
filtersState = filtersState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +125,8 @@ private fun DefaultRoomListTopBar(
|
||||||
onSearchClicked: () -> Unit,
|
onSearchClicked: () -> Unit,
|
||||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||||
displayMenuItems: Boolean,
|
displayMenuItems: Boolean,
|
||||||
|
displayFilters: Boolean,
|
||||||
|
filtersState: RoomListFiltersState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
// We need this to manually clip the top app bar in preview mode
|
// We need this to manually clip the top app bar in preview mode
|
||||||
|
|
@ -153,12 +163,11 @@ private fun DefaultRoomListTopBar(
|
||||||
titleLarge = collapsedTitleTextStyle
|
titleLarge = collapsedTitleTextStyle
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
MediumTopAppBar(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.onSizeChanged {
|
.onSizeChanged {
|
||||||
appBarHeight = it.height
|
appBarHeight = it.height
|
||||||
}
|
}
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
.avatarBloom(
|
.avatarBloom(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
background = if (ElementTheme.isLightTheme) {
|
background = if (ElementTheme.isLightTheme) {
|
||||||
|
|
@ -178,113 +187,104 @@ private fun DefaultRoomListTopBar(
|
||||||
DpSize.Unspecified
|
DpSize.Unspecified
|
||||||
},
|
},
|
||||||
bottomSoftEdgeColor = ElementTheme.materialColors.background,
|
bottomSoftEdgeColor = ElementTheme.materialColors.background,
|
||||||
bottomSoftEdgeAlpha = 1f - collapsedFraction,
|
bottomSoftEdgeAlpha = if (displayFilters) {
|
||||||
|
1f
|
||||||
|
} else {
|
||||||
|
1f - collapsedFraction
|
||||||
|
},
|
||||||
alpha = if (areSearchResultsDisplayed) 0f else 1f,
|
alpha = if (areSearchResultsDisplayed) 0f else 1f,
|
||||||
)
|
)
|
||||||
.statusBarsPadding(),
|
.statusBarsPadding(),
|
||||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
) {
|
||||||
containerColor = Color.Transparent,
|
MediumTopAppBar(
|
||||||
scrolledContainerColor = Color.Transparent,
|
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||||
),
|
containerColor = Color.Transparent,
|
||||||
title = {
|
scrolledContainerColor = Color.Transparent,
|
||||||
Text(text = stringResource(id = R.string.screen_roomlist_main_space_title))
|
),
|
||||||
},
|
title = {
|
||||||
navigationIcon = {
|
Text(text = stringResource(id = R.string.screen_roomlist_main_space_title))
|
||||||
IconButton(
|
},
|
||||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
navigationIcon = {
|
||||||
onClick = onOpenSettings
|
NavigationIcon(
|
||||||
) {
|
avatarData = avatarData,
|
||||||
if (avatarData != null) {
|
showAvatarIndicator = showAvatarIndicator,
|
||||||
Avatar(
|
onClick = onOpenSettings,
|
||||||
avatarData = avatarData!!,
|
)
|
||||||
contentDescription = stringResource(CommonStrings.common_settings),
|
},
|
||||||
)
|
actions = {
|
||||||
} else {
|
if (displayMenuItems) {
|
||||||
// Placeholder avatar until the avatarData is available
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.size(AvatarSize.CurrentUserTopBar.dp),
|
|
||||||
shape = CircleShape,
|
|
||||||
color = ElementTheme.colors.iconSecondary,
|
|
||||||
content = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (showAvatarIndicator) {
|
|
||||||
RedIndicatorAtom(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.5.dp)
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
if (displayMenuItems) {
|
|
||||||
IconButton(
|
|
||||||
onClick = onSearchClicked,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = CompoundIcons.Search(),
|
|
||||||
contentDescription = stringResource(CommonStrings.action_search),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
|
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { showMenu = !showMenu }
|
onClick = onSearchClicked,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.OverflowVertical(),
|
imageVector = CompoundIcons.Search(),
|
||||||
contentDescription = null,
|
contentDescription = stringResource(CommonStrings.action_search),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
|
||||||
expanded = showMenu,
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
onDismissRequest = { showMenu = false }
|
IconButton(
|
||||||
) {
|
onClick = { showMenu = !showMenu }
|
||||||
if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
|
) {
|
||||||
DropdownMenuItem(
|
Icon(
|
||||||
onClick = {
|
imageVector = CompoundIcons.OverflowVertical(),
|
||||||
showMenu = false
|
contentDescription = null,
|
||||||
onMenuActionClicked(RoomListMenuAction.InviteFriends)
|
|
||||||
},
|
|
||||||
text = { Text(stringResource(id = CommonStrings.action_invite)) },
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = CompoundIcons.ShareAndroid(),
|
|
||||||
tint = ElementTheme.materialColors.secondary,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM) {
|
DropdownMenu(
|
||||||
DropdownMenuItem(
|
expanded = showMenu,
|
||||||
onClick = {
|
onDismissRequest = { showMenu = false }
|
||||||
showMenu = false
|
) {
|
||||||
onMenuActionClicked(RoomListMenuAction.ReportBug)
|
if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
|
||||||
},
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
|
onClick = {
|
||||||
leadingIcon = {
|
showMenu = false
|
||||||
Icon(
|
onMenuActionClicked(RoomListMenuAction.InviteFriends)
|
||||||
imageVector = CompoundIcons.ChatProblem(),
|
},
|
||||||
tint = ElementTheme.materialColors.secondary,
|
text = { Text(stringResource(id = CommonStrings.action_invite)) },
|
||||||
contentDescription = null,
|
leadingIcon = {
|
||||||
)
|
Icon(
|
||||||
}
|
imageVector = CompoundIcons.ShareAndroid(),
|
||||||
)
|
tint = ElementTheme.materialColors.secondary,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
onMenuActionClicked(RoomListMenuAction.ReportBug)
|
||||||
|
},
|
||||||
|
text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.ChatProblem(),
|
||||||
|
tint = ElementTheme.materialColors.secondary,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
scrollBehavior = scrollBehavior,
|
||||||
scrollBehavior = scrollBehavior,
|
windowInsets = WindowInsets(0.dp),
|
||||||
windowInsets = WindowInsets(0.dp),
|
)
|
||||||
)
|
if (displayFilters) {
|
||||||
|
RoomListFiltersView(
|
||||||
|
state = filtersState,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.alpha(collapsedFraction)
|
.alpha(collapsedFraction)
|
||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
|
|
@ -293,6 +293,40 @@ private fun DefaultRoomListTopBar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavigationIcon(
|
||||||
|
avatarData: AvatarData?,
|
||||||
|
showAvatarIndicator: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
if (avatarData != null) {
|
||||||
|
Avatar(
|
||||||
|
avatarData = avatarData,
|
||||||
|
contentDescription = stringResource(CommonStrings.common_settings),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Placeholder avatar until the avatarData is available
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(AvatarSize.CurrentUserTopBar.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
color = ElementTheme.colors.iconSecondary,
|
||||||
|
content = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showAvatarIndicator) {
|
||||||
|
RedIndicatorAtom(
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -305,6 +339,8 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
onSearchClicked = {},
|
onSearchClicked = {},
|
||||||
displayMenuItems = true,
|
displayMenuItems = true,
|
||||||
|
displayFilters = true,
|
||||||
|
filtersState = aRoomListFiltersState(),
|
||||||
onMenuActionClicked = {},
|
onMenuActionClicked = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -321,6 +357,8 @@ internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
onSearchClicked = {},
|
onSearchClicked = {},
|
||||||
displayMenuItems = true,
|
displayMenuItems = true,
|
||||||
|
displayFilters = true,
|
||||||
|
filtersState = aRoomListFiltersState(),
|
||||||
onMenuActionClicked = {},
|
onMenuActionClicked = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,11 @@ 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.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
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
|
||||||
|
|
@ -62,19 +61,15 @@ class RoomListDataSource @Inject constructor(
|
||||||
roomListService
|
roomListService
|
||||||
.allRooms
|
.allRooms
|
||||||
.summaries
|
.summaries
|
||||||
.onStart {
|
|
||||||
// If we have no cached results, display a placeholder loading state
|
|
||||||
if (diffCache.isEmpty()) {
|
|
||||||
_allRooms.emit(RoomListRoomSummaryFactory.createFakeList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onEach { roomSummaries ->
|
.onEach { roomSummaries ->
|
||||||
replaceWith(roomSummaries)
|
replaceWith(roomSummaries)
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
val allRooms: SharedFlow<ImmutableList<RoomListRoomSummary>> = _allRooms
|
val allRooms: Flow<ImmutableList<RoomListRoomSummary>> = _allRooms
|
||||||
|
|
||||||
|
val loadingState = roomListService.allRooms.loadingState
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
private fun observeNotificationSettings() {
|
private fun observeNotificationSettings() {
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||||
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.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomListRoomSummaryFactory @Inject constructor(
|
class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
|
|
@ -52,12 +50,6 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createFakeList(): ImmutableList<RoomListRoomSummary> {
|
|
||||||
return List(16) {
|
|
||||||
createPlaceholder("!fakeRoom$it:domain")
|
|
||||||
}.toImmutableList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(roomSummary: RoomSummary.Filled): RoomListRoomSummary {
|
fun create(roomSummary: RoomSummary.Filled): RoomListRoomSummary {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
||||||
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -34,4 +36,7 @@ interface RoomListModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
|
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindMigrationScreenPresenter(presenter: MigrationScreenPresenter): Presenter<MigrationScreenState>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,12 @@ import io.element.android.features.roomlist.impl.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum class representing the different filters that can be applied to the room list.
|
* Enum class representing the different filters that can be applied to the room list.
|
||||||
* Order is important.
|
* Order is important, it'll be used as initial order in the UI.
|
||||||
*/
|
*/
|
||||||
enum class RoomListFilter(val stringResource: Int) {
|
enum class RoomListFilter(val stringResource: Int) {
|
||||||
Rooms(R.string.screen_roomlist_filter_rooms),
|
|
||||||
People(R.string.screen_roomlist_filter_people),
|
|
||||||
Unread(R.string.screen_roomlist_filter_unreads),
|
Unread(R.string.screen_roomlist_filter_unreads),
|
||||||
|
People(R.string.screen_roomlist_filter_people),
|
||||||
|
Rooms(R.string.screen_roomlist_filter_rooms),
|
||||||
Favourites(R.string.screen_roomlist_filter_favourites);
|
Favourites(R.string.screen_roomlist_filter_favourites);
|
||||||
|
|
||||||
val oppositeFilter: RoomListFilter?
|
val oppositeFilter: RoomListFilter?
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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.filters
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import io.element.android.features.roomlist.impl.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the resources for the empty state when filters are applied to the room list.
|
||||||
|
* @param title the title of the empty state
|
||||||
|
* @param subtitle the subtitle of the empty state
|
||||||
|
*/
|
||||||
|
data class RoomListFiltersEmptyStateResources(
|
||||||
|
@StringRes val title: Int,
|
||||||
|
@StringRes val subtitle: Int,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a [RoomListFiltersEmptyStateResources] from a list of selected filters.
|
||||||
|
*/
|
||||||
|
fun fromSelectedFilters(selectedFilters: List<RoomListFilter>): RoomListFiltersEmptyStateResources? {
|
||||||
|
return when {
|
||||||
|
selectedFilters.isEmpty() -> null
|
||||||
|
selectedFilters.size == 1 -> {
|
||||||
|
when (selectedFilters.first()) {
|
||||||
|
RoomListFilter.Unread -> RoomListFiltersEmptyStateResources(
|
||||||
|
title = R.string.screen_roomlist_filter_unreads_empty_state_title,
|
||||||
|
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
|
||||||
|
)
|
||||||
|
RoomListFilter.People -> RoomListFiltersEmptyStateResources(
|
||||||
|
title = R.string.screen_roomlist_filter_people_empty_state_title,
|
||||||
|
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
|
||||||
|
)
|
||||||
|
RoomListFilter.Rooms -> RoomListFiltersEmptyStateResources(
|
||||||
|
title = R.string.screen_roomlist_filter_rooms_empty_state_title,
|
||||||
|
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
|
||||||
|
)
|
||||||
|
RoomListFilter.Favourites -> RoomListFiltersEmptyStateResources(
|
||||||
|
title = R.string.screen_roomlist_filter_favourites_empty_state_title,
|
||||||
|
subtitle = R.string.screen_roomlist_filter_favourites_empty_state_subtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> RoomListFiltersEmptyStateResources(
|
||||||
|
title = R.string.screen_roomlist_filter_mixed_empty_state_title,
|
||||||
|
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,6 @@
|
||||||
package io.element.android.features.roomlist.impl.filters
|
package io.element.android.features.roomlist.impl.filters
|
||||||
|
|
||||||
sealed interface RoomListFiltersEvents {
|
sealed interface RoomListFiltersEvents {
|
||||||
data object ClearSelectedFilters : RoomListFiltersEvents
|
|
||||||
data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvents
|
data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvents
|
||||||
|
data object ClearSelectedFilters : RoomListFiltersEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionStrategy
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
|
@ -34,50 +32,36 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as Matrix
|
||||||
class RoomListFiltersPresenter @Inject constructor(
|
class RoomListFiltersPresenter @Inject constructor(
|
||||||
private val roomListService: RoomListService,
|
private val roomListService: RoomListService,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val filterSelectionStrategy: FilterSelectionStrategy,
|
||||||
) : Presenter<RoomListFiltersState> {
|
) : Presenter<RoomListFiltersState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomListFiltersState {
|
override fun present(): RoomListFiltersState {
|
||||||
val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomListFilters).collectAsState(false)
|
val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomListFilters).collectAsState(false)
|
||||||
var unselectedFilters: Set<RoomListFilter> by rememberSaveable {
|
val filters by filterSelectionStrategy.filterSelectionStates.collectAsState()
|
||||||
mutableStateOf(RoomListFilter.entries.toSet())
|
|
||||||
}
|
|
||||||
var selectedFilters: Set<RoomListFilter> by rememberSaveable {
|
|
||||||
mutableStateOf(emptySet())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateFilters(newSelectedFilters: Set<RoomListFilter>) {
|
|
||||||
selectedFilters = newSelectedFilters
|
|
||||||
unselectedFilters = RoomListFilter.entries.toSet() -
|
|
||||||
selectedFilters -
|
|
||||||
selectedFilters.mapNotNull { it.oppositeFilter }.toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleEvents(event: RoomListFiltersEvents) {
|
fun handleEvents(event: RoomListFiltersEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is RoomListFiltersEvents.ToggleFilter -> {
|
|
||||||
val newSelectedFilters = if (selectedFilters.contains(event.filter)) {
|
|
||||||
selectedFilters - event.filter
|
|
||||||
} else {
|
|
||||||
selectedFilters + event.filter
|
|
||||||
}
|
|
||||||
updateFilters(newSelectedFilters)
|
|
||||||
}
|
|
||||||
RoomListFiltersEvents.ClearSelectedFilters -> {
|
RoomListFiltersEvents.ClearSelectedFilters -> {
|
||||||
updateFilters(newSelectedFilters = emptySet())
|
filterSelectionStrategy.clear()
|
||||||
|
}
|
||||||
|
is RoomListFiltersEvents.ToggleFilter -> {
|
||||||
|
filterSelectionStrategy.toggle(event.filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(isFeatureEnabled) {
|
LaunchedEffect(isFeatureEnabled) {
|
||||||
if (!isFeatureEnabled) {
|
if (!isFeatureEnabled) {
|
||||||
updateFilters(emptySet())
|
filterSelectionStrategy.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(selectedFilters) {
|
LaunchedEffect(filters) {
|
||||||
val allRoomsFilter = MatrixRoomListFilter.All(
|
val allRoomsFilter = MatrixRoomListFilter.All(
|
||||||
selectedFilters.map { roomListFilter ->
|
filters
|
||||||
when (roomListFilter) {
|
.filter { it.isSelected }
|
||||||
|
.map { roomListFilter ->
|
||||||
|
when (roomListFilter.filter) {
|
||||||
RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group
|
RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group
|
||||||
RoomListFilter.People -> MatrixRoomListFilter.Category.People
|
RoomListFilter.People -> MatrixRoomListFilter.Category.People
|
||||||
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
|
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
|
||||||
|
|
@ -89,8 +73,7 @@ class RoomListFiltersPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
return RoomListFiltersState(
|
return RoomListFiltersState(
|
||||||
unselectedFilters = unselectedFilters.toPersistentList(),
|
filterSelectionStates = filters.toPersistentList(),
|
||||||
selectedFilters = selectedFilters.toPersistentList(),
|
|
||||||
isFeatureEnabled = isFeatureEnabled,
|
isFeatureEnabled = isFeatureEnabled,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,21 @@
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl.filters
|
package io.element.android.features.roomlist.impl.filters
|
||||||
|
|
||||||
|
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
data class RoomListFiltersState(
|
data class RoomListFiltersState(
|
||||||
val unselectedFilters: ImmutableList<RoomListFilter>,
|
val filterSelectionStates: ImmutableList<FilterSelectionState>,
|
||||||
val selectedFilters: ImmutableList<RoomListFilter>,
|
|
||||||
val isFeatureEnabled: Boolean,
|
val isFeatureEnabled: Boolean,
|
||||||
val eventSink: (RoomListFiltersEvents) -> Unit,
|
val eventSink: (RoomListFiltersEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val hasAnyFilterSelected = selectedFilters.isNotEmpty()
|
val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected }
|
||||||
|
|
||||||
|
fun selectedFilters(): ImmutableList<RoomListFilter> {
|
||||||
|
return filterSelectionStates
|
||||||
|
.filter { it.isSelected }
|
||||||
|
.map { it.filter }
|
||||||
|
.toPersistentList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@
|
||||||
package io.element.android.features.roomlist.impl.filters
|
package io.element.android.features.roomlist.impl.filters
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
class RoomListFiltersStateProvider : PreviewParameterProvider<RoomListFiltersState> {
|
class RoomListFiltersStateProvider : PreviewParameterProvider<RoomListFiltersState> {
|
||||||
|
|
@ -26,20 +25,17 @@ class RoomListFiltersStateProvider : PreviewParameterProvider<RoomListFiltersSta
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRoomListFiltersState(),
|
aRoomListFiltersState(),
|
||||||
aRoomListFiltersState(
|
aRoomListFiltersState(
|
||||||
selectedFilters = persistentListOf(RoomListFilter.Rooms, RoomListFilter.Favourites),
|
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
|
||||||
unselectedFilters = persistentListOf(RoomListFilter.Unread),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aRoomListFiltersState(
|
fun aRoomListFiltersState(
|
||||||
unselectedFilters: ImmutableList<RoomListFilter> = RoomListFilter.entries.toImmutableList(),
|
filterSelectionStates: List<FilterSelectionState> = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = false) },
|
||||||
selectedFilters: ImmutableList<RoomListFilter> = persistentListOf(),
|
|
||||||
isFeatureEnabled: Boolean = true,
|
isFeatureEnabled: Boolean = true,
|
||||||
eventSink: (RoomListFiltersEvents) -> Unit = {},
|
eventSink: (RoomListFiltersEvents) -> Unit = {},
|
||||||
) = RoomListFiltersState(
|
) = RoomListFiltersState(
|
||||||
unselectedFilters = unselectedFilters,
|
filterSelectionStates = filterSelectionStates.toImmutableList(),
|
||||||
selectedFilters = selectedFilters,
|
|
||||||
isFeatureEnabled = isFeatureEnabled,
|
isFeatureEnabled = isFeatureEnabled,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,22 @@
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl.filters
|
package io.element.android.features.roomlist.impl.filters
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.FilterChipDefaults
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -42,17 +40,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.libraries.designsystem.modifiers.fadingEdge
|
|
||||||
import io.element.android.libraries.designsystem.modifiers.horizontalFadingEdgesBrush
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListFiltersView(
|
fun RoomListFiltersView(
|
||||||
state: RoomListFiltersState,
|
state: RoomListFiltersState,
|
||||||
|
|
@ -62,53 +57,45 @@ fun RoomListFiltersView(
|
||||||
state.eventSink(RoomListFiltersEvents.ClearSelectedFilters)
|
state.eventSink(RoomListFiltersEvents.ClearSelectedFilters)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onFilterClicked(filter: RoomListFilter) {
|
fun onToggleFilter(filter: RoomListFilter) {
|
||||||
state.eventSink(RoomListFiltersEvents.ToggleFilter(filter))
|
state.eventSink(RoomListFiltersEvents.ToggleFilter(filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
val startPadding = if (state.hasAnyFilterSelected) 4.dp else 16.dp
|
val lazyListState = rememberLazyListState()
|
||||||
Row(
|
LazyRow(
|
||||||
modifier = modifier.padding(start = startPadding, end = 16.dp),
|
contentPadding = PaddingValues(start = 8.dp, end = 16.dp),
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
state = lazyListState,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(visible = state.hasAnyFilterSelected) {
|
item("clear_filters") {
|
||||||
RoomListClearFiltersButton(
|
if (state.hasAnyFilterSelected) {
|
||||||
modifier = Modifier.testTag(TestTags.homeScreenClearFilters),
|
RoomListClearFiltersButton(
|
||||||
onClick = ::onClearFiltersClicked
|
modifier = Modifier
|
||||||
)
|
.padding(start = 8.dp)
|
||||||
|
.testTag(TestTags.homeScreenClearFilters),
|
||||||
|
onClick = ::onClearFiltersClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val lazyListState = rememberLazyListState()
|
for (filterWithSelection in state.filterSelectionStates) {
|
||||||
val fadingEdgesBrush = horizontalFadingEdgesBrush(
|
item(filterWithSelection.filter) {
|
||||||
showLeft = lazyListState.canScrollBackward,
|
RoomListFilterView(
|
||||||
showRight = lazyListState.canScrollForward
|
modifier = Modifier.animateItemPlacement(),
|
||||||
)
|
roomListFilter = filterWithSelection.filter,
|
||||||
LazyRow(
|
selected = filterWithSelection.isSelected,
|
||||||
modifier = Modifier
|
onClick = ::onToggleFilter,
|
||||||
.fillMaxWidth()
|
)
|
||||||
.fadingEdge(fadingEdgesBrush),
|
}
|
||||||
state = lazyListState,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
roomListFilters(state.selectedFilters, selected = true, onClick = ::onFilterClicked)
|
|
||||||
roomListFilters(state.unselectedFilters, selected = false, onClick = ::onFilterClicked)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
LaunchedEffect(state.filterSelectionStates) {
|
||||||
|
// Checking for canScrollBackward is necessary for the itemPlacementAnimation to work correctly.
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
// We don't want the itemPlacementAnimation to be triggered when clearing the filters.
|
||||||
private fun LazyListScope.roomListFilters(
|
if (!state.hasAnyFilterSelected || lazyListState.canScrollBackward) {
|
||||||
filters: ImmutableList<RoomListFilter>,
|
lazyListState.animateScrollToItem(0)
|
||||||
selected: Boolean,
|
}
|
||||||
onClick: (RoomListFilter) -> Unit,
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = filters,
|
|
||||||
) { filter ->
|
|
||||||
RoomListFilterView(
|
|
||||||
roomListFilter = filter,
|
|
||||||
selected = selected,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,22 +104,18 @@ private fun RoomListClearFiltersButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
IconButton(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier
|
||||||
onClick = onClick,
|
.clip(CircleShape)
|
||||||
|
.background(ElementTheme.colors.bgActionPrimaryRest)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
) {
|
) {
|
||||||
Box(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier.align(Alignment.Center),
|
||||||
.clip(CircleShape)
|
imageVector = CompoundIcons.Close(),
|
||||||
.background(ElementTheme.colors.bgActionPrimaryRest)
|
tint = ElementTheme.colors.iconOnSolidPrimary,
|
||||||
) {
|
contentDescription = stringResource(id = io.element.android.libraries.ui.strings.R.string.action_clear),
|
||||||
Icon(
|
)
|
||||||
modifier = Modifier.align(Alignment.Center),
|
|
||||||
imageVector = CompoundIcons.Close(),
|
|
||||||
tint = ElementTheme.colors.iconOnSolidPrimary,
|
|
||||||
contentDescription = stringResource(id = io.element.android.libraries.ui.strings.R.string.action_clear),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,9 +129,7 @@ private fun RoomListFilterView(
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = { onClick(roomListFilter) },
|
onClick = { onClick(roomListFilter) },
|
||||||
modifier = modifier
|
modifier = modifier.height(36.dp),
|
||||||
.minimumInteractiveComponentSize()
|
|
||||||
.height(36.dp),
|
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
colors = FilterChipDefaults.filterChipColors(
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
containerColor = ElementTheme.colors.bgCanvasDefault,
|
containerColor = ElementTheme.colors.bgCanvasDefault,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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.filters.selection
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.roomlist.impl.filters.RoomListFilter
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStrategy {
|
||||||
|
private val selectedFilters = LinkedHashSet<RoomListFilter>()
|
||||||
|
|
||||||
|
override val filterSelectionStates = MutableStateFlow(buildFilters())
|
||||||
|
|
||||||
|
override fun select(filter: RoomListFilter) {
|
||||||
|
selectedFilters.add(filter)
|
||||||
|
filterSelectionStates.value = buildFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deselect(filter: RoomListFilter) {
|
||||||
|
selectedFilters.remove(filter)
|
||||||
|
filterSelectionStates.value = buildFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSelected(filter: RoomListFilter): Boolean {
|
||||||
|
return selectedFilters.contains(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
selectedFilters.clear()
|
||||||
|
filterSelectionStates.value = buildFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildFilters(): Set<FilterSelectionState> {
|
||||||
|
val selectedFilterStates = selectedFilters.map {
|
||||||
|
FilterSelectionState(
|
||||||
|
filter = it,
|
||||||
|
isSelected = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.mapNotNull { it.oppositeFilter }.toSet()
|
||||||
|
val unselectedFilterStates = unselectedFilters.map {
|
||||||
|
FilterSelectionState(
|
||||||
|
filter = it,
|
||||||
|
isSelected = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (selectedFilterStates + unselectedFilterStates).toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.filters.selection
|
||||||
|
|
||||||
|
import io.element.android.features.roomlist.impl.filters.RoomListFilter
|
||||||
|
|
||||||
|
data class FilterSelectionState(
|
||||||
|
val filter: RoomListFilter,
|
||||||
|
val isSelected: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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.filters.selection
|
||||||
|
|
||||||
|
import io.element.android.features.roomlist.impl.filters.RoomListFilter
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface FilterSelectionStrategy {
|
||||||
|
val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
|
||||||
|
|
||||||
|
fun select(filter: RoomListFilter)
|
||||||
|
fun deselect(filter: RoomListFilter)
|
||||||
|
fun isSelected(filter: RoomListFilter): Boolean
|
||||||
|
fun clear()
|
||||||
|
|
||||||
|
fun toggle(filter: RoomListFilter) {
|
||||||
|
if (isSelected(filter)) {
|
||||||
|
deselect(filter)
|
||||||
|
} else {
|
||||||
|
select(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,8 +33,7 @@ import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
||||||
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.model.createRoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
|
|
@ -54,13 +53,12 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
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.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
|
@ -75,6 +73,7 @@ import io.element.android.libraries.matrix.test.verification.FakeSessionVerifica
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.MutablePresenter
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
|
@ -86,7 +85,6 @@ import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
class RoomListPresenterTests {
|
class RoomListPresenterTests {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
|
@ -127,7 +125,6 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.showAvatarIndicator).isTrue()
|
assertThat(initialState.showAvatarIndicator).isTrue()
|
||||||
sessionVerificationService.givenCanVerifySession(false)
|
sessionVerificationService.givenCanVerifySession(false)
|
||||||
|
|
@ -169,11 +166,9 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = consumeItemsUntilPredicate(timeout = 3.seconds) { state -> state.roomList.dataOrNull()?.size == 16 }.last()
|
val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last()
|
||||||
// Room list is loaded with 16 placeholders
|
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
|
||||||
val initialItems = initialState.roomList.dataOrNull().orEmpty()
|
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||||
assertThat(initialItems.size).isEqualTo(16)
|
|
||||||
assertThat(initialItems.all { it.isPlaceholder }).isTrue()
|
|
||||||
roomListService.postAllRooms(
|
roomListService.postAllRooms(
|
||||||
listOf(
|
listOf(
|
||||||
aRoomSummaryFilled(
|
aRoomSummaryFilled(
|
||||||
|
|
@ -182,10 +177,10 @@ class RoomListPresenterTests {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val withRoomState = consumeItemsUntilPredicate { state -> state.roomList.dataOrNull()?.size == 1 }.last()
|
val withRoomsState =
|
||||||
val withRoomStateItems = withRoomState.roomList.dataOrNull().orEmpty()
|
consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Rooms && state.contentAsRooms().summaries.isNotEmpty() }.last()
|
||||||
assertThat(withRoomStateItems.size).isEqualTo(1)
|
assertThat(withRoomsState.contentAsRooms().summaries).hasSize(1)
|
||||||
assertThat(withRoomStateItems.first()).isEqualTo(
|
assertThat(withRoomsState.contentAsRooms().summaries.first()).isEqualTo(
|
||||||
createRoomListRoomSummary(
|
createRoomListRoomSummary(
|
||||||
numberOfUnreadMentions = 1,
|
numberOfUnreadMentions = 1,
|
||||||
numberOfUnreadMessages = 2,
|
numberOfUnreadMessages = 2,
|
||||||
|
|
@ -241,23 +236,28 @@ class RoomListPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle RecoveryKeyConfirmation last session`() = runTest {
|
fun `present - handle RecoveryKeyConfirmation last session`() = runTest {
|
||||||
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
||||||
|
val roomListService = FakeRoomListService().apply {
|
||||||
|
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||||
|
}
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
coroutineScope = scope,
|
coroutineScope = scope,
|
||||||
client = FakeMatrixClient(
|
client = FakeMatrixClient(
|
||||||
encryptionService = FakeEncryptionService().apply {
|
encryptionService = FakeEncryptionService().apply {
|
||||||
emitIsLastDevice(true)
|
emitIsLastDevice(true)
|
||||||
}
|
},
|
||||||
|
roomListService = roomListService
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
val eventSink = consumeItemsUntilPredicate {
|
||||||
val eventSink = awaitItem().eventSink
|
it.contentState is RoomListContentState.Rooms
|
||||||
|
}.last().eventSink
|
||||||
// For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation
|
// For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation
|
||||||
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
||||||
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
||||||
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None)
|
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -265,16 +265,22 @@ class RoomListPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
||||||
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
||||||
|
val roomListService = FakeRoomListService().apply {
|
||||||
|
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||||
|
}
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
|
client = FakeMatrixClient(roomListService = roomListService),
|
||||||
coroutineScope = scope,
|
coroutineScope = scope,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val eventSink = awaitItem().eventSink
|
val eventSink = consumeItemsUntilPredicate {
|
||||||
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification)
|
it.contentState is RoomListContentState.Rooms
|
||||||
|
}.last().eventSink
|
||||||
|
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification)
|
||||||
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
||||||
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None)
|
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -282,7 +288,11 @@ class RoomListPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle DismissRecoveryKeyPrompt`() = runTest {
|
fun `present - handle DismissRecoveryKeyPrompt`() = runTest {
|
||||||
val encryptionService = FakeEncryptionService()
|
val encryptionService = FakeEncryptionService()
|
||||||
|
val roomListService = FakeRoomListService().apply {
|
||||||
|
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||||
|
}
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
sessionVerificationService = FakeSessionVerificationService().apply {
|
sessionVerificationService = FakeSessionVerificationService().apply {
|
||||||
givenCanVerifySession(false)
|
givenCanVerifySession(false)
|
||||||
|
|
@ -297,15 +307,16 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
val initialState = consumeItemsUntilPredicate {
|
||||||
val initialState = awaitItem()
|
it.contentState is RoomListContentState.Rooms
|
||||||
assertThat(initialState.securityBannerState).isEqualTo(SecurityBannerState.None)
|
}.last()
|
||||||
|
assertThat(initialState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||||
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
val nextState = awaitItem()
|
val nextState = awaitItem()
|
||||||
assertThat(nextState.securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
assertThat(nextState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
||||||
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
|
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.securityBannerState).isEqualTo(SecurityBannerState.None)
|
assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -314,22 +325,30 @@ class RoomListPresenterTests {
|
||||||
fun `present - sets invite state`() = runTest {
|
fun `present - sets invite state`() = runTest {
|
||||||
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
|
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
|
||||||
val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow)
|
val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow)
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val presenter = createRoomListPresenter(inviteStateDataSource = inviteStateDataSource, coroutineScope = scope)
|
val presenter = createRoomListPresenter(
|
||||||
|
inviteStateDataSource = inviteStateDataSource,
|
||||||
|
coroutineScope = scope,
|
||||||
|
client = FakeMatrixClient(roomListService = roomListService),
|
||||||
|
)
|
||||||
|
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
consumeItemsUntilPredicate {
|
||||||
assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites)
|
it.contentState is RoomListContentState.Rooms
|
||||||
|
}
|
||||||
|
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
|
||||||
|
|
||||||
inviteStateFlow.value = InvitesState.SeenInvites
|
inviteStateFlow.value = InvitesState.SeenInvites
|
||||||
assertThat(awaitItem().invitesState).isEqualTo(InvitesState.SeenInvites)
|
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.SeenInvites)
|
||||||
|
|
||||||
inviteStateFlow.value = InvitesState.NewInvites
|
inviteStateFlow.value = InvitesState.NewInvites
|
||||||
assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NewInvites)
|
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NewInvites)
|
||||||
|
|
||||||
inviteStateFlow.value = InvitesState.NoInvites
|
inviteStateFlow.value = InvitesState.NoInvites
|
||||||
assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites)
|
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -477,6 +496,7 @@ class RoomListPresenterTests {
|
||||||
val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||||
val notificationSettingsService = FakeNotificationSettingsService()
|
val notificationSettingsService = FakeNotificationSettingsService()
|
||||||
val roomListService = FakeRoomListService()
|
val roomListService = FakeRoomListService()
|
||||||
|
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||||
roomListService.postAllRooms(listOf(aRoomSummaryFilled(notificationMode = userDefinedMode)))
|
roomListService.postAllRooms(listOf(aRoomSummaryFilled(notificationMode = userDefinedMode)))
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
|
|
@ -488,12 +508,13 @@ class RoomListPresenterTests {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, userDefinedMode)
|
notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, userDefinedMode)
|
||||||
|
|
||||||
val updatedState = consumeItemsUntilPredicate { state ->
|
val updatedState = consumeItemsUntilPredicate { state ->
|
||||||
state.roomList.dataOrNull().orEmpty().any { it.id == A_ROOM_ID.value && it.userDefinedNotificationMode == userDefinedMode }
|
(state.contentState as? RoomListContentState.Rooms)?.summaries.orEmpty().any { summary ->
|
||||||
|
summary.id == A_ROOM_ID.value && summary.userDefinedNotificationMode == userDefinedMode
|
||||||
|
}
|
||||||
}.last()
|
}.last()
|
||||||
|
|
||||||
val room = updatedState.roomList.dataOrNull()?.find { it.id == A_ROOM_ID.value }
|
val room = updatedState.contentAsRooms().summaries.find { it.id == A_ROOM_ID.value }
|
||||||
assertThat(room?.userDefinedNotificationMode).isEqualTo(userDefinedMode)
|
assertThat(room?.userDefinedNotificationMode).isEqualTo(userDefinedMode)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
|
@ -526,30 +547,46 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun `present - change in migration presenter state modifies isMigrating`() = runTest {
|
@Test
|
||||||
val client = FakeMatrixClient(sessionId = A_SESSION_ID)
|
fun `present - change in migration presenter state modifies contentState`() = runTest {
|
||||||
val migrationStore = InMemoryMigrationScreenStore()
|
val migrationScreenPresenter = MutablePresenter(MigrationScreenState(true))
|
||||||
val migrationScreenPresenter = MigrationScreenPresenter(client, migrationStore)
|
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = client,
|
|
||||||
coroutineScope = scope,
|
coroutineScope = scope,
|
||||||
migrationScreenPresenter = migrationScreenPresenter,
|
migrationScreenPresenter = migrationScreenPresenter,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
// The migration screen is shown if the migration screen has not been shown before
|
// The migration screen is shown if the migration screen has not been shown before
|
||||||
assertThat(initialState.displayMigrationStatus).isTrue()
|
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Migration::class.java)
|
||||||
skipItems(2)
|
|
||||||
|
|
||||||
// Set migration as done and set the room list service as running to trigger a refresh of the presenter value
|
// Set migration as done and set the room list service as running to trigger a refresh of the presenter value
|
||||||
(client.roomListService as FakeRoomListService).postState(RoomListService.State.Running)
|
migrationScreenPresenter.updateState(MigrationScreenState(false))
|
||||||
migrationStore.setMigrationScreenShown(A_SESSION_ID)
|
|
||||||
|
|
||||||
// The migration screen is not shown anymore
|
// The migration screen is not shown anymore
|
||||||
assertThat(awaitItem().displayMigrationStatus).isFalse()
|
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room service returns no room, then contentState is Empty `() = runTest {
|
||||||
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(0))
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
|
val presenter = createRoomListPresenter(
|
||||||
|
client = matrixClient,
|
||||||
|
coroutineScope = scope,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -609,10 +646,7 @@ class RoomListPresenterTests {
|
||||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter(
|
migrationScreenPresenter: Presenter<MigrationScreenState> = Presenter { MigrationScreenState(false) },
|
||||||
matrixClient = client,
|
|
||||||
migrationScreenStore = InMemoryMigrationScreenStore(),
|
|
||||||
),
|
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
||||||
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
internal fun RoomListState.contentAsRooms() = contentState as RoomListContentState.Rooms
|
||||||
|
|
@ -26,7 +26,6 @@ import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performTouchInput
|
import androidx.compose.ui.test.performTouchInput
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
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
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
|
@ -35,7 +34,6 @@ import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
|
|
@ -50,7 +48,7 @@ class RoomListViewTest {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
securityBannerState = SecurityBannerState.SessionVerification,
|
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -65,7 +63,7 @@ class RoomListViewTest {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
securityBannerState = SecurityBannerState.SessionVerification,
|
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
onVerifyClicked = callback,
|
onVerifyClicked = callback,
|
||||||
|
|
@ -79,7 +77,7 @@ class RoomListViewTest {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
securityBannerState = SecurityBannerState.RecoveryKeyConfirmation,
|
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -94,7 +92,7 @@ class RoomListViewTest {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
securityBannerState = SecurityBannerState.RecoveryKeyConfirmation,
|
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
onConfirmRecoveryKeyClicked = callback,
|
onConfirmRecoveryKeyClicked = callback,
|
||||||
|
|
@ -110,7 +108,7 @@ class RoomListViewTest {
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
roomList = AsyncData.Success(persistentListOf()),
|
contentState = anEmptyContentState(),
|
||||||
),
|
),
|
||||||
onCreateRoomClicked = callback,
|
onCreateRoomClicked = callback,
|
||||||
)
|
)
|
||||||
|
|
@ -124,7 +122,7 @@ class RoomListViewTest {
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
val room0 = state.roomList.dataOrNull()!!.first()
|
val room0 = state.contentAsRooms().summaries.first()
|
||||||
ensureCalledOnceWithParam(room0.roomId) { callback ->
|
ensureCalledOnceWithParam(room0.roomId) { callback ->
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
@ -140,7 +138,7 @@ class RoomListViewTest {
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
val room0 = state.roomList.dataOrNull()!!.first()
|
val room0 = state.contentAsRooms().summaries.first()
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
|
|
@ -170,7 +168,7 @@ class RoomListViewTest {
|
||||||
fun `clicking on invites invokes the expected callback`() {
|
fun `clicking on invites invokes the expected callback`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
invitesState = InvitesState.NewInvites,
|
contentState = aRoomsContentState(invitesState = InvitesState.NewInvites),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.filters
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.roomlist.impl.R
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RoomListFiltersEmptyStateResourcesTest {
|
||||||
|
@Test
|
||||||
|
fun `fromSelectedFilters should return null when selectedFilters is empty`() {
|
||||||
|
val selectedFilters = emptyList<RoomListFilter>()
|
||||||
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only unread filter`() {
|
||||||
|
val selectedFilters = listOf(RoomListFilter.Unread)
|
||||||
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
||||||
|
assertThat(result).isNotNull()
|
||||||
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_unreads_empty_state_title)
|
||||||
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only people filter`() {
|
||||||
|
val selectedFilters = listOf(RoomListFilter.People)
|
||||||
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
||||||
|
assertThat(result).isNotNull()
|
||||||
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_people_empty_state_title)
|
||||||
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only rooms filter`() {
|
||||||
|
val selectedFilters = listOf(RoomListFilter.Rooms)
|
||||||
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
||||||
|
assertThat(result).isNotNull()
|
||||||
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_rooms_empty_state_title)
|
||||||
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only favourites filter`() {
|
||||||
|
val selectedFilters = listOf(RoomListFilter.Favourites)
|
||||||
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
||||||
|
assertThat(result).isNotNull()
|
||||||
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_title)
|
||||||
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() {
|
||||||
|
val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites)
|
||||||
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
||||||
|
assertThat(result).isNotNull()
|
||||||
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title)
|
||||||
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy
|
||||||
|
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
|
@ -37,13 +39,12 @@ class RoomListFiltersPresenterTests {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.selectedFilters).isEmpty()
|
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
assertThat(state.unselectedFilters).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
RoomListFilter.Rooms,
|
filterSelectionState(RoomListFilter.Unread, false),
|
||||||
RoomListFilter.People,
|
filterSelectionState(RoomListFilter.People, false),
|
||||||
RoomListFilter.Unread,
|
filterSelectionState(RoomListFilter.Rooms, false),
|
||||||
RoomListFilter.Favourites,
|
filterSelectionState(RoomListFilter.Favourites, false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
@ -58,32 +59,33 @@ class RoomListFiltersPresenterTests {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
|
awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
|
||||||
|
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
|
|
||||||
assertThat(state.selectedFilters).containsExactly(RoomListFilter.Rooms)
|
|
||||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||||
assertThat(state.unselectedFilters).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
RoomListFilter.Unread,
|
filterSelectionState(RoomListFilter.Rooms, true),
|
||||||
RoomListFilter.Favourites,
|
filterSelectionState(RoomListFilter.Unread, false),
|
||||||
|
filterSelectionState(RoomListFilter.Favourites, false),
|
||||||
|
).inOrder()
|
||||||
|
|
||||||
|
assertThat(state.selectedFilters()).containsExactly(
|
||||||
|
RoomListFilter.Rooms,
|
||||||
)
|
)
|
||||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
||||||
assertThat(roomListCurrentFilter.filters).containsExactly(
|
assertThat(roomListCurrentFilter.filters).containsExactly(
|
||||||
MatrixRoomListFilter.Category.Group,
|
MatrixRoomListFilter.Category.Group,
|
||||||
)
|
)
|
||||||
|
|
||||||
state.eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
|
state.eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.selectedFilters).isEmpty()
|
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
assertThat(state.unselectedFilters).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
RoomListFilter.Rooms,
|
filterSelectionState(RoomListFilter.Unread, false),
|
||||||
RoomListFilter.People,
|
filterSelectionState(RoomListFilter.People, false),
|
||||||
RoomListFilter.Unread,
|
filterSelectionState(RoomListFilter.Rooms, false),
|
||||||
RoomListFilter.Favourites,
|
filterSelectionState(RoomListFilter.Favourites, false),
|
||||||
)
|
).inOrder()
|
||||||
|
assertThat(state.selectedFilters()).isEmpty()
|
||||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
||||||
assertThat(roomListCurrentFilter.filters).isEmpty()
|
assertThat(roomListCurrentFilter.filters).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
@ -99,24 +101,28 @@ class RoomListFiltersPresenterTests {
|
||||||
}.test {
|
}.test {
|
||||||
awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
|
awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.selectedFilters).isNotEmpty()
|
|
||||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||||
state.eventSink.invoke(RoomListFiltersEvents.ClearSelectedFilters)
|
state.eventSink.invoke(RoomListFiltersEvents.ClearSelectedFilters)
|
||||||
}
|
}
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.selectedFilters).isEmpty()
|
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createRoomListFiltersPresenter(
|
private fun filterSelectionState(filter: RoomListFilter, selected: Boolean) = FilterSelectionState(
|
||||||
|
filter = filter,
|
||||||
|
isSelected = selected,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createRoomListFiltersPresenter(
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
): RoomListFiltersPresenter {
|
): RoomListFiltersPresenter {
|
||||||
return RoomListFiltersPresenter(
|
return RoomListFiltersPresenter(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,11 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.roomlist.impl.R
|
import io.element.android.features.roomlist.impl.R
|
||||||
|
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.pressTag
|
import io.element.android.tests.testutils.pressTag
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
@ -56,8 +55,7 @@ class RoomListFiltersViewTests {
|
||||||
rule.setContent {
|
rule.setContent {
|
||||||
RoomListFiltersView(
|
RoomListFiltersView(
|
||||||
state = aRoomListFiltersState(
|
state = aRoomListFiltersState(
|
||||||
unselectedFilters = persistentListOf(),
|
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) },
|
||||||
selectedFilters = RoomListFilter.entries.toImmutableList(),
|
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import io.element.android.features.roomlist.impl.datasource.DefaultInviteStateDa
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
||||||
|
import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
||||||
import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore
|
import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource
|
import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource
|
||||||
|
|
@ -127,6 +128,7 @@ class RoomListScreen(
|
||||||
filtersPresenter = RoomListFiltersPresenter(
|
filtersPresenter = RoomListFiltersPresenter(
|
||||||
roomListService = matrixClient.roomListService,
|
roomListService = matrixClient.roomListService,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
||||||
),
|
),
|
||||||
analyticsService = NoopAnalyticsService(),
|
analyticsService = NoopAnalyticsService(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ dependencies {
|
||||||
implementation(libs.test.junit)
|
implementation(libs.test.junit)
|
||||||
implementation(libs.test.truth)
|
implementation(libs.test.truth)
|
||||||
implementation(libs.coroutines.test)
|
implementation(libs.coroutines.test)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.test.turbine)
|
implementation(libs.test.turbine)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.tests.testutils
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
class MutablePresenter<State>(initialState: State) : Presenter<State> {
|
||||||
|
private val stateFlow = MutableStateFlow(initialState)
|
||||||
|
|
||||||
|
fun updateState(state: State) {
|
||||||
|
stateFlow.value = state
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): State {
|
||||||
|
return stateFlow.collectAsState().value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ import app.cash.turbine.withTurbineTimeout
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consume all items until timeout is reached waiting for an event or we receive terminal event.
|
* Consume all items until timeout is reached waiting for an event or we receive terminal event.
|
||||||
|
|
@ -48,7 +49,7 @@ suspend fun <T : Any> ReceiveTurbine<T>.awaitLastSequentialItem(): T {
|
||||||
* @return the list of consumed items.
|
* @return the list of consumed items.
|
||||||
*/
|
*/
|
||||||
suspend fun <T : Any> ReceiveTurbine<T>.consumeItemsUntilPredicate(
|
suspend fun <T : Any> ReceiveTurbine<T>.consumeItemsUntilPredicate(
|
||||||
timeout: Duration = 100.milliseconds,
|
timeout: Duration = 3.seconds,
|
||||||
ignoreTimeoutError: Boolean = false,
|
ignoreTimeoutError: Boolean = false,
|
||||||
predicate: (T) -> Boolean,
|
predicate: (T) -> Boolean,
|
||||||
): List<T> {
|
): List<T> {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f29850d64823d7243ab2b6ec3f885fae2cf88aa763b18f96735094ebef9f19cd
|
|
||||||
size 36421
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4d429c0ce8f4f34e7155775689bf8bc2efa61e83db6704e225592a2ea2c494b6
|
||||||
|
size 48004
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:1f783da840d0b70462c9361cf5cd89f0c5636758925b99a4095ab82bab53fe10
|
|
||||||
size 42000
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:855f3544c544966b8ff7a4a83f02feb9326e9dc80dd792f6749575045fe6c951
|
||||||
|
size 52543
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:7b036518dc3f7d67d97a747f98a2909ff39461b4219a184200abc4e49d83e38b
|
|
||||||
size 36041
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9ec97a339d25fb7d0f928209567a81e13d65c2817ac21b372ff8573dc6d2f184
|
||||||
|
size 47603
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:4743f2334377effb907c28b0bf0f550cd06f47f59ae6b71bf46de1299cf7c890
|
|
||||||
size 41609
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ad5dee1ca507cbc94f4ba21141afb75c074766dc7ef25eaa13ea7f296bcd6e32
|
||||||
|
size 52194
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9097d88f12cd44680cdcef752d45b12884217c65bac846d3007f16393b3d7233
|
||||||
|
size 28036
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8f99dc73a1c501c8f3e2a61cce1f479b35cdf75fa3909a8a282f396d9ac93237
|
||||||
|
size 24013
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:01c07ac21d9ac4d9d916a4a15b27edc3d06f0f6206a736bc2ee5f0206c786523
|
||||||
|
size 16006
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7dd6178952670e12aa6f581793102afe44854457360bff654d7ca6089c8450ec
|
||||||
|
size 20112
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f4e503d1c883b1b1b5a98f979afc04a2bc65aa23ba674ca3b96bcab3d7888773
|
||||||
|
size 27993
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:30f05250641da8458b7891f881e6d50cd65f9e8d519672094df0ef7ed6c1afba
|
||||||
|
size 22795
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7ce4c893f2a0aa99384d16c531776604cbf2eab9040467f64b42a6e625fdb217
|
||||||
|
size 15264
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3c2301c797291b295a3433e2d26e2ec485f9e3b0ea825dea8794bb06fe608c48
|
||||||
|
size 19117
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue