mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Merge branch 'develop' into feature/bma/otherAccountUnreadCount
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
@@ -14,6 +14,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
interface HomeEntryPoint : FeatureEntryPoint {
|
||||
@@ -33,5 +34,6 @@ interface HomeEntryPoint : FeatureEntryPoint {
|
||||
fun navigateToRoomSettings(roomId: RoomId)
|
||||
fun navigateToBugReport()
|
||||
fun navigateToAddAccount()
|
||||
fun navigateToSession(sessionId: SessionId)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -8,10 +8,8 @@
|
||||
|
||||
package io.element.android.features.home.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
sealed interface HomeEvent {
|
||||
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvent
|
||||
data class SwitchToAccount(val sessionId: SessionId) : HomeEvent
|
||||
data object DismissMultiAccountAnnouncement : HomeEvent
|
||||
data object DismissSwipeAvatarAnnouncement : HomeEvent
|
||||
}
|
||||
|
||||
+1
@@ -222,6 +222,7 @@ class HomeFlowNode(
|
||||
onRoomClick = ::navigateToRoom,
|
||||
onSettingsClick = callback::navigateToSettings,
|
||||
onAddAccountClick = callback::navigateToAddAccount,
|
||||
onSwitchToAccount = callback::navigateToSession,
|
||||
onStartChatClick = callback::navigateToCreateRoom,
|
||||
onCreateSpaceClick = callback::navigateToCreateSpace,
|
||||
onSetUpRecoveryClick = callback::navigateToSetUpRecovery,
|
||||
|
||||
+9
-4
@@ -27,6 +27,7 @@ import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.indicator.api.IndicatorService
|
||||
@@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.totalUnreadCountFlow
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -51,12 +53,14 @@ class HomePresenter(
|
||||
private val sessionStore: SessionStore,
|
||||
private val announcementService: AnnouncementService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
@AppCoroutineScope
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
) : Presenter<HomeState> {
|
||||
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
|
||||
|
||||
@Composable
|
||||
override fun present(): HomeState {
|
||||
val coroutineState = rememberCoroutineScope()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val matrixUser by client.userProfile.collectAsState()
|
||||
val totalUnreadCount by remember { client.totalUnreadCountFlow() }
|
||||
// The initial value is only used for the first composition, remember it to avoid calling the SDK on every recomposition.
|
||||
@@ -114,10 +118,11 @@ class HomePresenter(
|
||||
is HomeEvent.SelectHomeNavigationBarItem -> {
|
||||
currentHomeNavigationBarItemOrdinal = event.item.ordinal
|
||||
}
|
||||
is HomeEvent.SwitchToAccount -> coroutineState.launch {
|
||||
sessionStore.setLatestSession(event.sessionId.value)
|
||||
// The user is switching account, which destroys this presenter, so use the app scope to not be cancelled.
|
||||
HomeEvent.DismissSwipeAvatarAnnouncement -> appCoroutineScope.launch {
|
||||
announcementService.onAnnouncementDismissed(Announcement.SwipeAvatarToSwitchAccount)
|
||||
}
|
||||
HomeEvent.DismissMultiAccountAnnouncement -> coroutineState.launch {
|
||||
HomeEvent.DismissMultiAccountAnnouncement -> coroutineScope.launch {
|
||||
announcementService.onAnnouncementDismissed(Announcement.MultiAccount)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -69,6 +69,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -78,6 +79,7 @@ fun HomeView(
|
||||
onRoomClick: (RoomId, EventId?) -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onAddAccountClick: () -> Unit,
|
||||
onSwitchToAccount: (SessionId) -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onStartChatClick: () -> Unit,
|
||||
@@ -130,6 +132,7 @@ fun HomeView(
|
||||
onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
|
||||
onCreateSpaceClick = { if (firstThrottler.canHandle()) onCreateSpaceClick() },
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
onSwitchToAccount = onSwitchToAccount,
|
||||
)
|
||||
|
||||
if (state.globalSearchState.isEnabled) {
|
||||
@@ -167,6 +170,7 @@ private fun HomeScaffold(
|
||||
onStartChatClick: () -> Unit,
|
||||
onCreateSpaceClick: () -> Unit,
|
||||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
onSwitchToAccount: (SessionId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onRoomClick(room: RoomListRoomSummary) {
|
||||
@@ -213,8 +217,10 @@ private fun HomeScaffold(
|
||||
},
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onAccountSwitch = {
|
||||
state.eventSink(HomeEvent.SwitchToAccount(it))
|
||||
onAccountSwitch = { sessionId ->
|
||||
// The user has switched account by swiping their avatar, so they know the gesture.
|
||||
state.eventSink(HomeEvent.DismissSwipeAvatarAnnouncement)
|
||||
onSwitchToAccount(sessionId)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
displayFilters = state.displayRoomListFilters,
|
||||
@@ -379,6 +385,7 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStatePreviewParam::class) sta
|
||||
onRoomClick = { _, _ -> },
|
||||
onSettingsClick = {},
|
||||
onAddAccountClick = {},
|
||||
onSwitchToAccount = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onStartChatClick = {},
|
||||
@@ -400,6 +407,7 @@ internal fun HomeViewA11yPreview() = ElementPreview {
|
||||
onRoomClick = { _, _ -> },
|
||||
onSettingsClick = {},
|
||||
onAddAccountClick = {},
|
||||
onSwitchToAccount = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onStartChatClick = {},
|
||||
|
||||
+6
-7
@@ -16,13 +16,11 @@ import io.element.android.libraries.designsystem.components.Announcement
|
||||
import io.element.android.libraries.designsystem.components.AnnouncementType
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.push.api.battery.BatteryOptimizationEvent
|
||||
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
||||
|
||||
@Composable
|
||||
internal fun BatteryOptimizationBanner(
|
||||
state: BatteryOptimizationState,
|
||||
onActionClick: () -> Unit,
|
||||
onDismissClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Announcement(
|
||||
@@ -31,8 +29,8 @@ internal fun BatteryOptimizationBanner(
|
||||
description = stringResource(R.string.banner_battery_optimization_content_android),
|
||||
type = AnnouncementType.Actionable(
|
||||
actionText = stringResource(R.string.banner_battery_optimization_submit_android),
|
||||
onActionClick = { state.eventSink(BatteryOptimizationEvent.RequestDisableOptimizations) },
|
||||
onDismissClick = { state.eventSink(BatteryOptimizationEvent.Dismiss) },
|
||||
onActionClick = onActionClick,
|
||||
onDismissClick = onDismissClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -41,6 +39,7 @@ internal fun BatteryOptimizationBanner(
|
||||
@Composable
|
||||
internal fun BatteryOptimizationBannerPreview() = ElementPreview {
|
||||
BatteryOptimizationBanner(
|
||||
state = aBatteryOptimizationState(),
|
||||
onActionClick = {},
|
||||
onDismissClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
+8
-7
@@ -16,14 +16,12 @@ import io.element.android.libraries.designsystem.components.Announcement
|
||||
import io.element.android.libraries.designsystem.components.AnnouncementType
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvent
|
||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun FullScreenIntentPermissionBanner(
|
||||
state: FullScreenIntentPermissionsState,
|
||||
onActionClick: () -> Unit,
|
||||
onDismissClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Announcement(
|
||||
@@ -31,8 +29,8 @@ fun FullScreenIntentPermissionBanner(
|
||||
description = stringResource(R.string.full_screen_intent_banner_message),
|
||||
type = AnnouncementType.Actionable(
|
||||
actionText = stringResource(CommonStrings.action_continue),
|
||||
onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvent.Dismiss) },
|
||||
onActionClick = { state.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) },
|
||||
onDismissClick = onDismissClick,
|
||||
onActionClick = onActionClick,
|
||||
),
|
||||
modifier = modifier.roomListBannerPadding(),
|
||||
)
|
||||
@@ -42,6 +40,9 @@ fun FullScreenIntentPermissionBanner(
|
||||
@Composable
|
||||
internal fun FullScreenIntentPermissionBannerPreview() {
|
||||
ElementPreview {
|
||||
FullScreenIntentPermissionBanner(aFullScreenIntentPermissionsState())
|
||||
FullScreenIntentPermissionBanner(
|
||||
onActionClick = {},
|
||||
onDismissClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -92,6 +92,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
||||
@Composable
|
||||
fun HomeTopBar(
|
||||
@@ -307,9 +308,12 @@ private fun NavigationIcon(
|
||||
// Listen to page changes and switch account if needed
|
||||
val latestOnAccountSwitch by rememberUpdatedState(onAccountSwitch)
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.settledPage }.collect { page ->
|
||||
latestOnAccountSwitch(SessionId(currentUserAndNeighbors[page].userId.value))
|
||||
}
|
||||
snapshotFlow { pagerState.settledPage }
|
||||
// The current account is on page 1, so there is nothing to switch to
|
||||
.filter { page -> page != 1 }
|
||||
.collect { page ->
|
||||
latestOnAccountSwitch(SessionId(currentUserAndNeighbors[page].userId.value))
|
||||
}
|
||||
}
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
|
||||
+135
-56
@@ -8,6 +8,12 @@
|
||||
|
||||
package io.element.android.features.home.impl.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -15,7 +21,9 @@ 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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
@@ -24,6 +32,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
@@ -39,10 +48,11 @@ import io.element.android.features.home.impl.filters.aRoomListFiltersState
|
||||
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
||||
import io.element.android.features.home.impl.roomlist.RoomListBanner
|
||||
import io.element.android.features.home.impl.roomlist.RoomListContentState
|
||||
import io.element.android.features.home.impl.roomlist.RoomListContentStatePreviewParam
|
||||
import io.element.android.features.home.impl.roomlist.RoomListEvent
|
||||
import io.element.android.features.home.impl.roomlist.SecurityBannerState
|
||||
import io.element.android.features.home.impl.roomlist.toRoomListBanner
|
||||
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
|
||||
import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
@@ -52,6 +62,8 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvent
|
||||
import io.element.android.libraries.push.api.battery.BatteryOptimizationEvent
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@@ -158,22 +170,20 @@ private fun EmptyView(
|
||||
},
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
Box {
|
||||
when (state.securityBannerState) {
|
||||
SecurityBannerState.SetUpRecovery -> {
|
||||
SetUpRecoveryKeyBanner(
|
||||
onContinueClick = onSetUpRecoveryClick,
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||
)
|
||||
}
|
||||
SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClick = onConfirmRecoveryKeyClick,
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||
)
|
||||
}
|
||||
SecurityBannerState.None -> Unit
|
||||
}
|
||||
RoomListBannerContainer(
|
||||
banner = state.securityBannerState.toRoomListBanner(),
|
||||
) { banner ->
|
||||
RoomListBannerView(
|
||||
banner = banner,
|
||||
onActionClick = {
|
||||
when (banner) {
|
||||
RoomListBanner.SetUpRecovery -> onSetUpRecoveryClick()
|
||||
RoomListBanner.RecoveryKeyConfirmation -> onConfirmRecoveryKeyClick()
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,54 +237,56 @@ private fun RoomsViewList(
|
||||
lazyListState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val banner = state.toRoomListBanner()
|
||||
// The banner slot must never be 0px high, else the LazyColumn considers that it is not visible and it is
|
||||
// removed from the composition. A new banner would then be added above the visible area of the list.
|
||||
val bannerSlotMinHeight = with(LocalDensity.current) { 1.toDp() }
|
||||
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||
eventSink(RoomListEvent.UpdateVisibleRange(visibleRange))
|
||||
// The first item of the list is the banner slot, remove it to get the range of the rooms
|
||||
val roomsRange = (visibleRange.first - 1).coerceAtLeast(0)..<visibleRange.last
|
||||
eventSink(RoomListEvent.UpdateVisibleRange(roomsRange))
|
||||
}
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
when (state.securityBannerState) {
|
||||
SecurityBannerState.SetUpRecovery -> {
|
||||
item {
|
||||
SetUpRecoveryKeyBanner(
|
||||
onContinueClick = onSetUpRecoveryClick,
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||
)
|
||||
}
|
||||
}
|
||||
SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||
item {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClick = onConfirmRecoveryKeyClick,
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||
)
|
||||
}
|
||||
}
|
||||
// Banner precedence (top-to-bottom): full-screen-intent > battery-optimization >
|
||||
// new-notification-sound > sound-unavailable. At most one renders at a time.
|
||||
SecurityBannerState.None -> when {
|
||||
state.fullScreenIntentPermissionsState.shouldDisplayBanner -> {
|
||||
item {
|
||||
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
|
||||
}
|
||||
}
|
||||
state.batteryOptimizationState.shouldDisplayBanner -> {
|
||||
item {
|
||||
BatteryOptimizationBanner(state = state.batteryOptimizationState)
|
||||
}
|
||||
}
|
||||
state.showNewNotificationSoundBanner -> {
|
||||
item {
|
||||
NewNotificationSoundBanner(
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissNewNotificationSoundBanner) },
|
||||
)
|
||||
}
|
||||
}
|
||||
// The banner slot is always the first item of the list, even when there is no banner to display,
|
||||
// so that the rooms keep the same index when a banner is added or removed. Since the rooms do not
|
||||
// have a key, this is required to keep the scroll position.
|
||||
item {
|
||||
RoomListBannerContainer(
|
||||
banner = banner,
|
||||
modifier = Modifier.heightIn(min = bannerSlotMinHeight),
|
||||
) { displayedBanner ->
|
||||
RoomListBannerView(
|
||||
banner = displayedBanner,
|
||||
onActionClick = {
|
||||
when (displayedBanner) {
|
||||
RoomListBanner.SetUpRecovery -> onSetUpRecoveryClick()
|
||||
RoomListBanner.RecoveryKeyConfirmation -> onConfirmRecoveryKeyClick()
|
||||
RoomListBanner.FullScreenIntentPermission ->
|
||||
state.fullScreenIntentPermissionsState.eventSink(FullScreenIntentPermissionsEvent.OpenSettings)
|
||||
RoomListBanner.BatteryOptimization ->
|
||||
state.batteryOptimizationState.eventSink(BatteryOptimizationEvent.RequestDisableOptimizations)
|
||||
RoomListBanner.NewNotificationSound,
|
||||
RoomListBanner.SwipeAvatar -> Unit
|
||||
}
|
||||
},
|
||||
onDismissClick = {
|
||||
when (displayedBanner) {
|
||||
RoomListBanner.SetUpRecovery,
|
||||
RoomListBanner.RecoveryKeyConfirmation -> eventSink(RoomListEvent.DismissBanner)
|
||||
RoomListBanner.FullScreenIntentPermission ->
|
||||
state.fullScreenIntentPermissionsState.eventSink(FullScreenIntentPermissionsEvent.Dismiss)
|
||||
RoomListBanner.BatteryOptimization -> state.batteryOptimizationState.eventSink(BatteryOptimizationEvent.Dismiss)
|
||||
RoomListBanner.NewNotificationSound -> eventSink(RoomListEvent.DismissNewNotificationSoundBanner)
|
||||
RoomListBanner.SwipeAvatar -> eventSink(RoomListEvent.DismissSwipeAvatarBanner)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
@@ -298,6 +310,73 @@ private fun RoomsViewList(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the [banner] at the top of the room list, and animates every change: when the banner is
|
||||
* dismissed, when it is replaced by another banner, and when a banner appears.
|
||||
* The size of the slot is animated, so that the items below it are not moved abruptly.
|
||||
*
|
||||
* @param banner the banner to display, or null if there is no banner to display.
|
||||
* @param modifier the modifier to apply to the slot.
|
||||
* @param content renders the given banner.
|
||||
*/
|
||||
@Composable
|
||||
private fun RoomListBannerContainer(
|
||||
banner: RoomListBanner?,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable (RoomListBanner) -> Unit,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = banner,
|
||||
// Fill the width so that only the height is animated
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
transitionSpec = {
|
||||
fadeIn(tween(durationMillis = HALF_DURATION_MILLIS, delayMillis = HALF_DURATION_MILLIS)) togetherWith
|
||||
fadeOut(tween(durationMillis = HALF_DURATION_MILLIS)) using
|
||||
SizeTransform { _, _ -> tween(durationMillis = 2 * HALF_DURATION_MILLIS) }
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
label = "RoomListBannerContainer",
|
||||
) { targetBanner ->
|
||||
if (targetBanner != null) {
|
||||
content(targetBanner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val HALF_DURATION_MILLIS = 150
|
||||
|
||||
@Composable
|
||||
private fun RoomListBannerView(
|
||||
banner: RoomListBanner,
|
||||
onActionClick: () -> Unit,
|
||||
onDismissClick: () -> Unit,
|
||||
) {
|
||||
when (banner) {
|
||||
RoomListBanner.SetUpRecovery -> SetUpRecoveryKeyBanner(
|
||||
onContinueClick = onActionClick,
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
RoomListBanner.RecoveryKeyConfirmation -> ConfirmRecoveryKeyBanner(
|
||||
onContinueClick = onActionClick,
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
RoomListBanner.FullScreenIntentPermission -> FullScreenIntentPermissionBanner(
|
||||
onActionClick = onActionClick,
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
RoomListBanner.BatteryOptimization -> BatteryOptimizationBanner(
|
||||
onActionClick = onActionClick,
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
RoomListBanner.NewNotificationSound -> NewNotificationSoundBanner(
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
RoomListBanner.SwipeAvatar -> SwipeAvatarBanner(
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyViewForFilterStates(
|
||||
selectedFilters: ImmutableList<RoomListFilter>,
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.home.impl.R
|
||||
import io.element.android.libraries.designsystem.components.Announcement
|
||||
import io.element.android.libraries.designsystem.components.AnnouncementType
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
internal fun SwipeAvatarBanner(
|
||||
onDismissClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Announcement(
|
||||
modifier = modifier.roomListBannerPadding(),
|
||||
title = stringResource(R.string.banner_swipe_avatar_title),
|
||||
description = stringResource(R.string.banner_swipe_avatar_content),
|
||||
type = AnnouncementType.Actionable(
|
||||
actionText = stringResource(CommonStrings.action_ok),
|
||||
onActionClick = onDismissClick,
|
||||
onDismissClick = onDismissClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SwipeAvatarBannerPreview() = ElementPreview {
|
||||
SwipeAvatarBanner(
|
||||
onDismissClick = {},
|
||||
)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.roomlist
|
||||
|
||||
/**
|
||||
* The banners which can be displayed at the top of the room list. At most one banner is displayed at a time.
|
||||
*/
|
||||
internal enum class RoomListBanner {
|
||||
SetUpRecovery,
|
||||
RecoveryKeyConfirmation,
|
||||
FullScreenIntentPermission,
|
||||
BatteryOptimization,
|
||||
NewNotificationSound,
|
||||
SwipeAvatar,
|
||||
}
|
||||
|
||||
internal fun SecurityBannerState.toRoomListBanner(): RoomListBanner? = when (this) {
|
||||
SecurityBannerState.SetUpRecovery -> RoomListBanner.SetUpRecovery
|
||||
SecurityBannerState.RecoveryKeyConfirmation -> RoomListBanner.RecoveryKeyConfirmation
|
||||
SecurityBannerState.None -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Banner precedence (top-to-bottom): security > full-screen-intent > battery-optimization >
|
||||
* new-notification-sound > swipe-avatar. At most one renders at a time.
|
||||
*/
|
||||
internal fun RoomListContentState.Rooms.toRoomListBanner(): RoomListBanner? {
|
||||
return securityBannerState.toRoomListBanner() ?: when {
|
||||
fullScreenIntentPermissionsState.shouldDisplayBanner -> RoomListBanner.FullScreenIntentPermission
|
||||
batteryOptimizationState.shouldDisplayBanner -> RoomListBanner.BatteryOptimization
|
||||
showNewNotificationSoundBanner -> RoomListBanner.NewNotificationSound
|
||||
showSwipeAvatarBanner -> RoomListBanner.SwipeAvatar
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
+5
@@ -30,12 +30,16 @@ open class RoomListContentStatePreviewParam : PreviewParameterProvider<RoomListC
|
||||
aRoomsContentState(
|
||||
showNewNotificationSoundBanner = true,
|
||||
),
|
||||
aRoomsContentState(
|
||||
showSwipeAvatarBanner = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRoomsContentState(
|
||||
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
||||
showNewNotificationSoundBanner: Boolean = false,
|
||||
showSwipeAvatarBanner: Boolean = false,
|
||||
showUnreadCount: Boolean = false,
|
||||
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
||||
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
|
||||
@@ -45,6 +49,7 @@ internal fun aRoomsContentState(
|
||||
) = RoomListContentState.Rooms(
|
||||
securityBannerState = securityBannerState,
|
||||
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
||||
showSwipeAvatarBanner = showSwipeAvatarBanner,
|
||||
showUnreadCount = showUnreadCount,
|
||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
|
||||
batteryOptimizationState = batteryOptimizationState,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ sealed interface RoomListEvent {
|
||||
data object DismissRequestVerificationPrompt : RoomListEvent
|
||||
data object DismissBanner : RoomListEvent
|
||||
data object DismissNewNotificationSoundBanner : RoomListEvent
|
||||
data object DismissSwipeAvatarBanner : RoomListEvent
|
||||
data object ToggleSearchResults : RoomListEvent
|
||||
data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvent
|
||||
|
||||
|
||||
+18
@@ -56,6 +56,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
||||
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
@@ -64,6 +65,7 @@ import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -89,6 +91,7 @@ class RoomListPresenter(
|
||||
private val spaceFiltersPresenter: Presenter<SpaceFiltersState>,
|
||||
private val globalSearchPresenter: Presenter<GlobalSearchState>,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val sessionStore: SessionStore,
|
||||
) : Presenter<RoomListState> {
|
||||
private val encryptionService = client.encryptionService
|
||||
|
||||
@@ -112,6 +115,15 @@ class RoomListPresenter(
|
||||
announcements.contains(Announcement.NewNotificationSound)
|
||||
}
|
||||
}.collectAsState(false)
|
||||
val showSwipeAvatarBanner by remember {
|
||||
combine(
|
||||
announcementService.announcementsToShowFlow(),
|
||||
sessionStore.sessionsFlow(),
|
||||
) { announcements, sessions ->
|
||||
// The swipe gesture on the avatar is only available when there are several accounts
|
||||
sessions.size >= 2 && announcements.contains(Announcement.SwipeAvatarToSwitchAccount)
|
||||
}
|
||||
}.collectAsState(false)
|
||||
|
||||
// Avatar indicator
|
||||
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
|
||||
@@ -133,6 +145,9 @@ class RoomListPresenter(
|
||||
RoomListEvent.DismissNewNotificationSoundBanner -> coroutineScope.launch {
|
||||
announcementService.onAnnouncementDismissed(Announcement.NewNotificationSound)
|
||||
}
|
||||
RoomListEvent.DismissSwipeAvatarBanner -> coroutineScope.launch {
|
||||
announcementService.onAnnouncementDismissed(Announcement.SwipeAvatarToSwitchAccount)
|
||||
}
|
||||
RoomListEvent.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvent.ToggleSearchVisibility)
|
||||
is RoomListEvent.ShowContextMenu -> {
|
||||
coroutineScope.showContextMenu(event, contextMenu)
|
||||
@@ -176,6 +191,7 @@ class RoomListPresenter(
|
||||
val contentState = roomListContentState(
|
||||
securityBannerDismissed,
|
||||
showNewNotificationSoundBanner,
|
||||
showSwipeAvatarBanner,
|
||||
showUnreadCount,
|
||||
showAllActivity,
|
||||
)
|
||||
@@ -235,6 +251,7 @@ class RoomListPresenter(
|
||||
private fun roomListContentState(
|
||||
securityBannerDismissed: Boolean,
|
||||
showNewNotificationSoundBanner: Boolean,
|
||||
showSwipeAvatarBanner: Boolean,
|
||||
showUnreadCount: Boolean,
|
||||
showAllActivity: Boolean,
|
||||
): RoomListContentState {
|
||||
@@ -265,6 +282,7 @@ class RoomListPresenter(
|
||||
RoomListContentState.Rooms(
|
||||
securityBannerState = securityBannerState,
|
||||
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
||||
showSwipeAvatarBanner = showSwipeAvatarBanner,
|
||||
showUnreadCount = showUnreadCount,
|
||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
|
||||
batteryOptimizationState = batteryOptimizationPresenter.present(),
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ sealed interface RoomListContentState {
|
||||
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
|
||||
val batteryOptimizationState: BatteryOptimizationState,
|
||||
val showNewNotificationSoundBanner: Boolean,
|
||||
val showSwipeAvatarBanner: Boolean,
|
||||
val showUnreadCount: Boolean,
|
||||
val summaries: ImmutableList<RoomListRoomSummary>,
|
||||
val seenRoomInvites: ImmutableSet<RoomId>,
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="banner_set_up_recovery_content">"Your chats are automatically backed up with end-to-end encryption. To restore this backup and retain your digital identity when you lose access to all your devices, you will need your recovery key."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Get recovery key"</string>
|
||||
<string name="banner_set_up_recovery_title">"Back up your chats"</string>
|
||||
<string name="banner_swipe_avatar_content">"Swipe up or down on your avatar to switch between your accounts without going to the app settings."</string>
|
||||
<string name="banner_swipe_avatar_title">"Swipe your avatar to switch accounts"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Confirm your recovery key to maintain access to your key storage and message history."</string>
|
||||
<string name="confirm_recovery_key_banner_primary_button_title">"Enter your recovery key"</string>
|
||||
<string name="confirm_recovery_key_banner_secondary_button_title">"Forgot your recovery key?"</string>
|
||||
|
||||
+2
@@ -14,6 +14,7 @@ import io.element.android.features.home.api.HomeEntryPoint
|
||||
import io.element.android.features.invite.test.declineandblock.FakeDeclineInviteAndBlockEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
@@ -53,6 +54,7 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
|
||||
override fun navigateToRoomSettings(roomId: RoomId) = lambdaError()
|
||||
override fun navigateToBugReport() = lambdaError()
|
||||
override fun navigateToAddAccount() = lambdaError()
|
||||
override fun navigateToSession(sessionId: SessionId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
|
||||
+27
-1
@@ -41,9 +41,12 @@ import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -122,6 +125,7 @@ class HomePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - total unread count is updated on sync heartbeat`() = runTest {
|
||||
var unreadCount = 0L
|
||||
@@ -142,6 +146,7 @@ class HomePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - total unread count defaults to 0 when the SDK call fails`() = runTest {
|
||||
val syncService = FakeSyncService()
|
||||
@@ -289,6 +294,25 @@ class HomePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - dismissing the swipe avatar announcement`() = runTest {
|
||||
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
||||
val presenter = createHomePresenter(
|
||||
announcementService = FakeAnnouncementService(
|
||||
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
||||
),
|
||||
appCoroutineScope = this,
|
||||
)
|
||||
presenter.test {
|
||||
val state = expectMostRecentItem()
|
||||
state.eventSink(HomeEvent.DismissSwipeAvatarAnnouncement)
|
||||
runCurrent()
|
||||
onAnnouncementDismissedResult.assertions().isCalledOnce()
|
||||
.with(value(Announcement.SwipeAvatarToSwitchAccount))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - total unread count is loaded in the initial state`() = runTest {
|
||||
val presenter = createHomePresenter(
|
||||
@@ -300,7 +324,7 @@ class HomePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createHomePresenter(
|
||||
internal fun TestScope.createHomePresenter(
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
syncService: SyncService = FakeSyncService(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
@@ -310,6 +334,7 @@ internal fun createHomePresenter(
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
announcementService: AnnouncementService = FakeAnnouncementService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
appCoroutineScope: CoroutineScope = backgroundScope,
|
||||
) = HomePresenter(
|
||||
client = client,
|
||||
syncService = syncService,
|
||||
@@ -321,4 +346,5 @@ internal fun createHomePresenter(
|
||||
sessionStore = sessionStore,
|
||||
announcementService = announcementService,
|
||||
featureFlagService = featureFlagService,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
)
|
||||
|
||||
+48
@@ -56,6 +56,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||
@@ -72,6 +73,9 @@ import io.element.android.libraries.preferences.test.InMemorySessionPreferencesS
|
||||
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.services.analytics.test.watchers.FakeAnalyticsColdStartWatcher
|
||||
@@ -645,6 +649,48 @@ class RoomListPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - swipe avatar banner`() = runTest {
|
||||
val roomList = FakeDynamicRoomList(
|
||||
summaries = MutableStateFlow(listOf(aRoomSummary())),
|
||||
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||
)
|
||||
val roomListService = FakeRoomListService(
|
||||
createRoomListLambda = { roomList },
|
||||
subscribeToVisibleRoomsLambda = { },
|
||||
)
|
||||
val matrixClient = FakeMatrixClient(
|
||||
roomListService = roomListService,
|
||||
)
|
||||
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
||||
val announcementService = FakeAnnouncementService(
|
||||
initialAnnouncementsToShowFlowValue = listOf(Announcement.SwipeAvatarToSwitchAccount),
|
||||
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
||||
)
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(aSessionData(sessionId = A_SESSION_ID.value)),
|
||||
)
|
||||
val presenter = createRoomListPresenter(
|
||||
client = matrixClient,
|
||||
announcementService = announcementService,
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2) // Skip initial state and intermediate event for loading the value of the feature flags
|
||||
val state = awaitItem()
|
||||
// Only one account, the banner is not displayed
|
||||
assertThat(state.contentAsRooms().showSwipeAvatarBanner).isFalse()
|
||||
sessionStore.addSession(aSessionData(sessionId = A_SESSION_ID_2.value))
|
||||
assertThat(awaitItem().contentAsRooms().showSwipeAvatarBanner).isTrue()
|
||||
state.eventSink(RoomListEvent.DismissSwipeAvatarBanner)
|
||||
onAnnouncementDismissedResult.assertions().isCalledOnce()
|
||||
.with(value(Announcement.SwipeAvatarToSwitchAccount))
|
||||
// Simulate service updating the value
|
||||
announcementService.emitAnnouncementsToShow(emptyList())
|
||||
assertThat(awaitItem().contentAsRooms().showSwipeAvatarBanner).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - showAllActivity and showUnreadCount reflect the feature flag values`() = runTest {
|
||||
val roomList = FakeDynamicRoomList(
|
||||
@@ -724,6 +770,7 @@ class RoomListPresenterTest {
|
||||
announcementService: AnnouncementService = FakeAnnouncementService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
markRoomAsRead: MarkRoomAsRead? = null,
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
) = RoomListPresenter(
|
||||
client = client,
|
||||
leaveRoomPresenter = { leaveRoomState },
|
||||
@@ -756,5 +803,6 @@ class RoomListPresenterTest {
|
||||
announcementService = announcementService,
|
||||
coldStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||
featureFlagService = featureFlagService,
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
}
|
||||
|
||||
+36
-9
@@ -51,9 +51,10 @@ class RoomListViewTest : RobolectricTest() {
|
||||
)
|
||||
)
|
||||
|
||||
// The banner is not included in the range, only the rooms are
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
RoomListEvent.UpdateVisibleRange(0..5),
|
||||
RoomListEvent.UpdateVisibleRange(0..4),
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -64,13 +65,15 @@ class RoomListViewTest : RobolectricTest() {
|
||||
setRoomListView(
|
||||
state = aRoomListState(
|
||||
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
|
||||
eventSink = eventsRecorder,
|
||||
// Ignore the visible range updates, which are also emitted when the banner is shrinking
|
||||
eventSink = { event ->
|
||||
if (event !is RoomListEvent.UpdateVisibleRange) {
|
||||
eventsRecorder(event)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
// Remove automatic initial events
|
||||
eventsRecorder.clear()
|
||||
|
||||
val close = activity!!.getString(CommonStrings.action_close)
|
||||
onNodeWithContentDescription(close).performClick()
|
||||
eventsRecorder.assertSingle(RoomListEvent.DismissBanner)
|
||||
@@ -82,18 +85,41 @@ class RoomListViewTest : RobolectricTest() {
|
||||
setRoomListView(
|
||||
state = aRoomListState(
|
||||
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
|
||||
eventSink = eventsRecorder,
|
||||
// Ignore the visible range updates, which are also emitted when the banner is shrinking
|
||||
eventSink = { event ->
|
||||
if (event !is RoomListEvent.UpdateVisibleRange) {
|
||||
eventsRecorder(event)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
// Remove automatic initial events
|
||||
eventsRecorder.clear()
|
||||
|
||||
val close = activity!!.getString(CommonStrings.action_close)
|
||||
onNodeWithContentDescription(close).performClick()
|
||||
eventsRecorder.assertSingle(RoomListEvent.DismissBanner)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on close swipe avatar banner emits the expected Event`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||
setRoomListView(
|
||||
state = aRoomListState(
|
||||
contentState = aRoomsContentState(showSwipeAvatarBanner = true),
|
||||
// Ignore the visible range updates, which are also emitted when the banner is shrinking
|
||||
eventSink = { event ->
|
||||
if (event !is RoomListEvent.UpdateVisibleRange) {
|
||||
eventsRecorder(event)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
eventsRecorder.assertEmpty()
|
||||
val close = activity!!.getString(CommonStrings.action_close)
|
||||
onNodeWithContentDescription(close).performClick()
|
||||
eventsRecorder.assertSingle(RoomListEvent.DismissSwipeAvatarBanner)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on continue recovery key banner invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||
@@ -305,6 +331,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setRoomListView(
|
||||
onRoomClick = { roomId, _ -> onRoomClick(roomId) },
|
||||
onSettingsClick = onSettingsClick,
|
||||
onAddAccountClick = onAddAccountClick,
|
||||
onSwitchToAccount = EnsureNeverCalledWithParam(),
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onStartChatClick = onCreateRoomClick,
|
||||
|
||||
Reference in New Issue
Block a user