RoomList: refactor and fix tests
This commit is contained in:
parent
6ac66d08fd
commit
3cb189f475
20 changed files with 252 additions and 107 deletions
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
open class RoomListContentStateProvider : PreviewParameterProvider<RoomListContentState> {
|
||||||
|
override val values: Sequence<RoomListContentState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aRoomsContentState(),
|
||||||
|
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)
|
||||||
|
|
@ -40,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
|
||||||
|
|
@ -89,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> {
|
||||||
|
|
@ -196,23 +196,23 @@ class RoomListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
val loadingState by roomListDataSource.loadingState.collectAsState()
|
val loadingState by roomListDataSource.loadingState.collectAsState()
|
||||||
val showMigration = migrationScreenPresenter.present().isMigrating
|
val showMigration = migrationScreenPresenter.present().isMigrating
|
||||||
val showSkeleton by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val showEmpty by remember {
|
val showEmpty by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
|
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val showSkeleton by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
return when {
|
return when {
|
||||||
showMigration -> RoomListContentState.Migration
|
showMigration -> RoomListContentState.Migration
|
||||||
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
|
||||||
showEmpty -> {
|
showEmpty -> {
|
||||||
val invitesState = inviteStateDataSource.inviteState()
|
val invitesState = inviteStateDataSource.inviteState()
|
||||||
RoomListContentState.Empty(invitesState)
|
RoomListContentState.Empty(invitesState)
|
||||||
}
|
}
|
||||||
|
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
||||||
else -> {
|
else -> {
|
||||||
val invitesState = inviteStateDataSource.inviteState()
|
val invitesState = inviteStateDataSource.inviteState()
|
||||||
val securityBannerState by securityBannerState(securityBannerDismissed)
|
val securityBannerState by securityBannerState(securityBannerDismissed)
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ enum class SecurityBannerState {
|
||||||
RecoveryKeyConfirmation,
|
RecoveryKeyConfirmation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
sealed interface RoomListContentState {
|
sealed interface RoomListContentState {
|
||||||
data object Migration : RoomListContentState
|
data object Migration : RoomListContentState
|
||||||
data class Skeleton(val count: Int) : RoomListContentState
|
data class Skeleton(val count: Int) : RoomListContentState
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ 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
|
||||||
|
|
@ -35,7 +34,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
|
|
||||||
open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
override val values: Sequence<RoomListState>
|
override val values: Sequence<RoomListState>
|
||||||
|
|
@ -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,16 +58,13 @@ 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,
|
||||||
|
|
@ -80,11 +75,7 @@ internal fun aRoomListState(
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
contentState = RoomListContentState.Rooms(
|
contentState = contentState,
|
||||||
invitesState = invitesState,
|
|
||||||
securityBannerState = securityBannerState,
|
|
||||||
summaries = roomList.dataOrNull().orEmpty().toPersistentList(),
|
|
||||||
),
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.roomlist.impl.components
|
package io.element.android.features.roomlist.impl.components
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.background
|
|
||||||
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.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -40,6 +39,7 @@ 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.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
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
|
||||||
|
|
@ -48,19 +48,24 @@ import io.element.android.features.roomlist.impl.InvitesEntryPointView
|
||||||
import io.element.android.features.roomlist.impl.InvitesState
|
import io.element.android.features.roomlist.impl.InvitesState
|
||||||
import io.element.android.features.roomlist.impl.R
|
import io.element.android.features.roomlist.impl.R
|
||||||
import io.element.android.features.roomlist.impl.RoomListContentState
|
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.RoomListEvents
|
||||||
import io.element.android.features.roomlist.impl.SecurityBannerState
|
import io.element.android.features.roomlist.impl.SecurityBannerState
|
||||||
import io.element.android.features.roomlist.impl.contentType
|
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.RoomListFilter
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersEmptyStateResources
|
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.RoomListFiltersState
|
||||||
|
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
|
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.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.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListContentView(
|
fun RoomListContentView(
|
||||||
|
|
@ -214,7 +219,6 @@ private fun RoomsViewList(
|
||||||
modifier = modifier.nestedScroll(nestedScrollConnection),
|
modifier = modifier.nestedScroll(nestedScrollConnection),
|
||||||
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
|
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
|
||||||
contentPadding = PaddingValues(bottom = 80.dp)
|
contentPadding = PaddingValues(bottom = 80.dp)
|
||||||
|
|
||||||
) {
|
) {
|
||||||
when (state.securityBannerState) {
|
when (state.securityBannerState) {
|
||||||
SecurityBannerState.SessionVerification -> {
|
SecurityBannerState.SessionVerification -> {
|
||||||
|
|
@ -261,7 +265,7 @@ private fun RoomsViewList(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptyViewForFilterStates(
|
private fun EmptyViewForFilterStates(
|
||||||
selectedFilters: List<RoomListFilter>,
|
selectedFilters: ImmutableList<RoomListFilter>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return
|
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return
|
||||||
|
|
@ -301,3 +305,18 @@ private fun EmptyScaffold(
|
||||||
action?.invoke(this)
|
action?.invoke(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStateProvider::class) state: RoomListContentState) = ElementPreview {
|
||||||
|
RoomListContentView(
|
||||||
|
contentState = state,
|
||||||
|
filtersState = aRoomListFiltersState(),
|
||||||
|
eventSink = {},
|
||||||
|
onVerifyClicked = { },
|
||||||
|
onConfirmRecoveryKeyClicked = { },
|
||||||
|
onRoomClicked = {},
|
||||||
|
onRoomLongClicked = {},
|
||||||
|
onCreateRoomClicked = { },
|
||||||
|
onInvitesClicked = { })
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,9 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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,7 +50,6 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ data class RoomListFiltersEmptyStateResources(
|
||||||
@StringRes val title: Int,
|
@StringRes val title: Int,
|
||||||
@StringRes val subtitle: Int,
|
@StringRes val subtitle: Int,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Create a [RoomListFiltersEmptyStateResources] from a list of selected filters.
|
* Create a [RoomListFiltersEmptyStateResources] from a list of selected filters.
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,11 @@ class RoomListFiltersPresenter @Inject constructor(
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val filterSelectionStrategy: FilterSelectionStrategy,
|
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)
|
||||||
val filters by filterSelectionStrategy.filterSelectionStates.collectAsState()
|
val filters by filterSelectionStrategy.filterSelectionStates.collectAsState()
|
||||||
|
|
||||||
|
|
||||||
fun handleEvents(event: RoomListFiltersEvents) {
|
fun handleEvents(event: RoomListFiltersEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
RoomListFiltersEvents.ClearSelectedFilters -> {
|
RoomListFiltersEvents.ClearSelectedFilters -> {
|
||||||
|
|
@ -75,7 +72,6 @@ class RoomListFiltersPresenter @Inject constructor(
|
||||||
roomListService.allRooms.updateFilter(allRoomsFilter)
|
roomListService.allRooms.updateFilter(allRoomsFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return RoomListFiltersState(
|
return RoomListFiltersState(
|
||||||
filterSelectionStates = filters.toPersistentList(),
|
filterSelectionStates = filters.toPersistentList(),
|
||||||
isFeatureEnabled = isFeatureEnabled,
|
isFeatureEnabled = isFeatureEnabled,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl.filters
|
||||||
|
|
||||||
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
|
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 filterSelectionStates: ImmutableList<FilterSelectionState>,
|
val filterSelectionStates: ImmutableList<FilterSelectionState>,
|
||||||
|
|
@ -26,7 +27,10 @@ data class RoomListFiltersState(
|
||||||
) {
|
) {
|
||||||
val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected }
|
val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected }
|
||||||
|
|
||||||
fun selectedFilters(): List<RoomListFilter> {
|
fun selectedFilters(): ImmutableList<RoomListFilter> {
|
||||||
return filterSelectionStates.filter { it.isSelected }.map { it.filter }
|
return filterSelectionStates
|
||||||
|
.filter { it.isSelected }
|
||||||
|
.map { it.filter }
|
||||||
|
.toPersistentList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ fun RoomListFiltersView(
|
||||||
state: RoomListFiltersState,
|
state: RoomListFiltersState,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun onClearFiltersClicked() {
|
fun onClearFiltersClicked() {
|
||||||
state.eventSink(RoomListFiltersEvents.ClearSelectedFilters)
|
state.eventSink(RoomListFiltersEvents.ClearSelectedFilters)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStrategy {
|
class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStrategy {
|
||||||
|
|
||||||
private val selectedFilters = LinkedHashSet<RoomListFilter>()
|
private val selectedFilters = LinkedHashSet<RoomListFilter>()
|
||||||
|
|
||||||
override val filterSelectionStates = MutableStateFlow(buildFilters())
|
override val filterSelectionStates = MutableStateFlow(buildFilters())
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import io.element.android.features.roomlist.impl.filters.RoomListFilter
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface FilterSelectionStrategy {
|
interface FilterSelectionStrategy {
|
||||||
|
|
||||||
val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
|
val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
|
||||||
|
|
||||||
fun select(filter: RoomListFilter)
|
fun select(filter: RoomListFilter)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -127,7 +126,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 +167,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(timeout = 3.seconds) { 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 +178,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 +237,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 +266,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 +289,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 +308,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 +326,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 +497,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 +509,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 +548,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 +647,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 ->
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue