diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt index 69def559a1..56a09a1f11 100644 --- a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt +++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt @@ -19,4 +19,6 @@ sealed interface Announcement { data object NewNotificationSound : Announcement data object MultiAccount : Announcement + + data object SwipeAvatarToSwitchAccount : Announcement } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt index 3c36989571..caf0001ee3 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt @@ -34,6 +34,7 @@ class DefaultAnnouncementService( announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show) } Announcement.MultiAccount -> showAnnouncementIfNeverShown(announcement) + Announcement.SwipeAvatarToSwitchAccount -> showAnnouncementIfNeverShown(announcement) } } @@ -46,7 +47,8 @@ class DefaultAnnouncementService( flowOf(Unit), announcementStore.announcementStatusFlow(Announcement.NewNotificationSound), announcementStore.announcementStatusFlow(Announcement.MultiAccount), - ) { _, newNotificationSoundStatus, multiAccountStatus -> + announcementStore.announcementStatusFlow(Announcement.SwipeAvatarToSwitchAccount), + ) { _, newNotificationSoundStatus, multiAccountStatus, swipeAvatarToSwitchAccountStatus -> buildList { if (newNotificationSoundStatus == AnnouncementStatus.Show) { add(Announcement.NewNotificationSound) @@ -56,6 +58,10 @@ class DefaultAnnouncementService( if (multiAccountStatus != AnnouncementStatus.Shown) { add(Announcement.MultiAccount) } + // The swipe avatar announcement is pending until it is dismissed, or the user switches account by swiping. + if (swipeAvatarToSwitchAccountStatus != AnnouncementStatus.Shown) { + add(Announcement.SwipeAvatarToSwitchAccount) + } } } } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt index 2235285bcf..4e2e11ee2e 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.map private val spaceAnnouncementKey = intPreferencesKey("spaceAnnouncement") private val newNotificationSoundKey = intPreferencesKey("newNotificationSound") private val multiAccountKey = intPreferencesKey("multiAccountAnnouncement") +private val swipeAvatarToSwitchAccountKey = intPreferencesKey("swipeAvatarToSwitchAccountAnnouncement") @ContributesBinding(AppScope::class) class DefaultAnnouncementStore( @@ -42,10 +43,13 @@ class DefaultAnnouncementStore( // - NewNotificationSound is opted in by AppMigration08 // - MultiAccount is NeverShown by default, which means pending. It is set to Shown when the user enters // the login flow while the feature is enabled (see NotLoggedInFlowNode), or dismisses the announcement. + // - SwipeAvatarToSwitchAccount is NeverShown by default, which means pending. It is set to Shown when the user + // dismisses the banner, or switches account by swiping their avatar. val defaultStatus = when (announcement) { Announcement.Fullscreen.Space -> AnnouncementStatus.Shown Announcement.NewNotificationSound -> AnnouncementStatus.Shown Announcement.MultiAccount -> AnnouncementStatus.NeverShown + Announcement.SwipeAvatarToSwitchAccount -> AnnouncementStatus.NeverShown } return store.data.map { prefs -> val ordinal = prefs[key] ?: defaultStatus.ordinal @@ -62,4 +66,5 @@ private fun Announcement.toKey() = when (this) { Announcement.Fullscreen.Space -> spaceAnnouncementKey Announcement.NewNotificationSound -> newNotificationSoundKey Announcement.MultiAccount -> multiAccountKey + Announcement.SwipeAvatarToSwitchAccount -> swipeAvatarToSwitchAccountKey } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt index 18442b362d..f9df1dbaaf 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt @@ -67,6 +67,22 @@ class DefaultAnnouncementServiceTest { assertThat(announcementStore.announcementStatusFlow(Announcement.MultiAccount).first()).isEqualTo(AnnouncementStatus.Shown) } + @Test + fun `when showing SwipeAvatarToSwitchAccount announcement, announcement is set to show only if it was never shown`() = runTest { + val announcementStore = InMemoryAnnouncementStore() + val sut = createDefaultAnnouncementService( + announcementStore = announcementStore, + ) + assertThat(announcementStore.announcementStatusFlow(Announcement.SwipeAvatarToSwitchAccount).first()).isEqualTo(AnnouncementStatus.NeverShown) + sut.showAnnouncement(Announcement.SwipeAvatarToSwitchAccount) + assertThat(announcementStore.announcementStatusFlow(Announcement.SwipeAvatarToSwitchAccount).first()).isEqualTo(AnnouncementStatus.Show) + // Simulate user close the announcement + sut.onAnnouncementDismissed(Announcement.SwipeAvatarToSwitchAccount) + // Calling again showAnnouncement should not change the value + sut.showAnnouncement(Announcement.SwipeAvatarToSwitchAccount) + assertThat(announcementStore.announcementStatusFlow(Announcement.SwipeAvatarToSwitchAccount).first()).isEqualTo(AnnouncementStatus.Shown) + } + @Test fun `test announcementsToShowFlow`() = runTest { val announcementStore = InMemoryAnnouncementStore() @@ -74,16 +90,18 @@ class DefaultAnnouncementServiceTest { announcementStore = announcementStore, ) sut.announcementsToShowFlow().test { - // MultiAccount is pending by default - assertThat(awaitItem()).containsExactly(Announcement.MultiAccount) + // MultiAccount and SwipeAvatarToSwitchAccount are pending by default + assertThat(awaitItem()).containsExactly(Announcement.MultiAccount, Announcement.SwipeAvatarToSwitchAccount) announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show) - assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound, Announcement.MultiAccount) + assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound, Announcement.MultiAccount, Announcement.SwipeAvatarToSwitchAccount) announcementStore.setAnnouncementStatus(Announcement.MultiAccount, AnnouncementStatus.Show) // Still pending - assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound, Announcement.MultiAccount) + assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound, Announcement.MultiAccount, Announcement.SwipeAvatarToSwitchAccount) announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown) - assertThat(awaitItem()).containsExactly(Announcement.MultiAccount) + assertThat(awaitItem()).containsExactly(Announcement.MultiAccount, Announcement.SwipeAvatarToSwitchAccount) announcementStore.setAnnouncementStatus(Announcement.MultiAccount, AnnouncementStatus.Shown) + assertThat(awaitItem()).containsExactly(Announcement.SwipeAvatarToSwitchAccount) + announcementStore.setAnnouncementStatus(Announcement.SwipeAvatarToSwitchAccount, AnnouncementStatus.Shown) assertThat(awaitItem()).isEmpty() } } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt index 9b5e762eb6..14a8eea9ab 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt @@ -17,10 +17,12 @@ class InMemoryAnnouncementStore( initialFullscreenAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, initialMultiAccountAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, + initialSwipeAvatarToSwitchAccountAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, ) : AnnouncementStore { private val fullScreenAnnouncement = MutableStateFlow(initialFullscreenAnnouncementStatus) private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus) private val multiAccountAnnouncement = MutableStateFlow(initialMultiAccountAnnouncementStatus) + private val swipeAvatarToSwitchAccountAnnouncement = MutableStateFlow(initialSwipeAvatarToSwitchAccountAnnouncementStatus) override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) { announcement.toMutableStateFlow().value = status @@ -34,11 +36,13 @@ class InMemoryAnnouncementStore( fullScreenAnnouncement.value = AnnouncementStatus.NeverShown newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown multiAccountAnnouncement.value = AnnouncementStatus.NeverShown + swipeAvatarToSwitchAccountAnnouncement.value = AnnouncementStatus.NeverShown } private fun Announcement.toMutableStateFlow() = when (this) { is Announcement.Fullscreen -> fullScreenAnnouncement Announcement.NewNotificationSound -> newNotificationSoundAnnouncement Announcement.MultiAccount -> multiAccountAnnouncement + Announcement.SwipeAvatarToSwitchAccount -> swipeAvatarToSwitchAccountAnnouncement } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvent.kt index abb0b1b837..7d071cf90d 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvent.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvent.kt @@ -11,4 +11,5 @@ package io.element.android.features.home.impl sealed interface HomeEvent { data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvent data object DismissMultiAccountAnnouncement : HomeEvent + data object DismissSwipeAvatarAnnouncement : HomeEvent } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index 1938e714eb..c250569976 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -28,6 +28,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.MatrixClient import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.combine @@ -56,13 +58,15 @@ class HomePresenter( private val sessionStore: SessionStore, private val announcementService: AnnouncementService, private val featureFlagService: FeatureFlagService, + @AppCoroutineScope + private val appCoroutineScope: CoroutineScope, ) : Presenter { private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder() @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @Composable override fun present(): HomeState { - val coroutineState = rememberCoroutineScope() + val coroutineScope = rememberCoroutineScope() val matrixUser by client.userProfile.collectAsState() val totalUnreadCount by produceState(client.totalUnreadCount().getOrDefault(0).toInt()) { @@ -126,7 +130,11 @@ class HomePresenter( is HomeEvent.SelectHomeNavigationBarItem -> { currentHomeNavigationBarItemOrdinal = event.item.ordinal } - HomeEvent.DismissMultiAccountAnnouncement -> coroutineState.launch { + // 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 -> coroutineScope.launch { announcementService.onAnnouncementDismissed(Announcement.MultiAccount) } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index f9e5809ed5..6640120614 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -217,7 +217,11 @@ private fun HomeScaffold( }, onMenuActionClick = onMenuActionClick, onOpenSettings = onOpenSettings, - onAccountSwitch = onSwitchToAccount, + 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, filtersState = roomListState.filtersState, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt index 95826665fe..3feabca8b0 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt @@ -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 = {}, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt index f2826415d2..ae5a25a623 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt @@ -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 = {}, + ) } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index e799f44e25..291b9f3c00 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -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, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index 5fc6d4c85e..02288eb8b7 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -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).. { - 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, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SwipeAvatarBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SwipeAvatarBanner.kt new file mode 100644 index 0000000000..bf1df99aec --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SwipeAvatarBanner.kt @@ -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 = {}, + ) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListBanner.kt new file mode 100644 index 0000000000..c273cc9ce4 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListBanner.kt @@ -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 + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStatePreviewParam.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStatePreviewParam.kt index e24074e042..bcc369d1bc 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStatePreviewParam.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStatePreviewParam.kt @@ -30,12 +30,16 @@ open class RoomListContentStatePreviewParam : PreviewParameterProvider = 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, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt index 49d033682f..d912842b64 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt @@ -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 diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index 68aba21188..611f1e8ee2 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -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, private val globalSearchPresenter: Presenter, private val featureFlagService: FeatureFlagService, + private val sessionStore: SessionStore, ) : Presenter { 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(), diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt index bb1ee631a1..6d3d85ab56 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -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, val seenRoomInvites: ImmutableSet, diff --git a/features/home/impl/src/main/res/values/localazy.xml b/features/home/impl/src/main/res/values/localazy.xml index 4f3420fa23..d1983621ac 100644 --- a/features/home/impl/src/main/res/values/localazy.xml +++ b/features/home/impl/src/main/res/values/localazy.xml @@ -8,6 +8,8 @@ "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." "Get recovery key" "Back up your chats" + "Swipe up or down on your avatar to switch between your accounts without going to the app settings." + "Swipe your avatar to switch accounts" "Confirm your recovery key to maintain access to your key storage and message history." "Enter your recovery key" "Forgot your recovery key?" diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index fa0d40017e..8bd779ef05 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -39,8 +39,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 @@ -119,6 +123,7 @@ class HomePresenterTest { } } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - total unread count is updated on sync heartbeat`() = runTest { var unreadCount = 0L @@ -136,6 +141,7 @@ class HomePresenterTest { } } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - total unread count defaults to 0 when the SDK call fails`() = runTest { val syncService = FakeSyncService() @@ -280,6 +286,25 @@ class HomePresenterTest { } } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - dismissing the swipe avatar announcement`() = runTest { + val onAnnouncementDismissedResult = lambdaRecorder { } + 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( @@ -291,7 +316,7 @@ class HomePresenterTest { } } -internal fun createHomePresenter( +internal fun TestScope.createHomePresenter( client: MatrixClient = FakeMatrixClient(), syncService: SyncService = FakeSyncService(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), @@ -301,6 +326,7 @@ internal fun createHomePresenter( sessionStore: SessionStore = InMemorySessionStore(), announcementService: AnnouncementService = FakeAnnouncementService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), + appCoroutineScope: CoroutineScope = backgroundScope, ) = HomePresenter( client = client, syncService = syncService, @@ -312,4 +338,5 @@ internal fun createHomePresenter( sessionStore = sessionStore, announcementService = announcementService, featureFlagService = featureFlagService, + appCoroutineScope = appCoroutineScope, ) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index f73e39b8d7..f45e0fa7bf 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -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 { } + 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, ) } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt index c6d185550c..61ac7b8dc6 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt @@ -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() + 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() diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png index 8106e587cf..5081cf144a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96a167627e1af6e5db7ecc39efb0d52a2ad3e23f84c2cd1222f088102fd22303 -size 41053 +oid sha256:7965b2152ccf703ca1ce05103835360e5a6c105708ddda2af2f3ebe08311d627 +size 41025 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_6_en.png new file mode 100644 index 0000000000..2e8b5c11be --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bae5109ecc9377e0f1d299a69bb5cc66d7a9432de3590d4a5e308abde7db61a9 +size 64511 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png index a6ccbb245e..5975364c78 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:875e9afdf2d3b6b67ad2376a788b2ec92fa3099a4b6882ee6834da1ec4dad7de -size 40836 +oid sha256:3f2a815d81ef6d332ca4a76ea82cc20cced9069f82defece60ff7d6ba6c3bdd9 +size 40914 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_6_en.png new file mode 100644 index 0000000000..e5d57b8a2a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04b58c0184ac8b9ff9b59c53125db66b890efbf09443f53e1f9091e2894519e4 +size 63677 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_SwipeAvatarBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_SwipeAvatarBanner_Day_0_en.png new file mode 100644 index 0000000000..3b74f9c1ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_SwipeAvatarBanner_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a9bab9bae4bd59e54e7dc2897367b110458baf184d9abb4ad801b76d6cb1432 +size 28089 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_SwipeAvatarBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_SwipeAvatarBanner_Night_0_en.png new file mode 100644 index 0000000000..ed0bcd0d48 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_SwipeAvatarBanner_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ead5c82cfb8ccac0a666e3c9d41121206932457ffd1047f268390b4f71ef198e +size 27439 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png index 8534820c63..2adbae0e28 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70793555aa11eede96e92e3d9a9186c75c4056a5f193bb5a3962227208777866 -size 143038 +oid sha256:932bd7fc62a301bd910dec13461b10bbc3a5d8df3dd7047f20ebc2d357b88f31 +size 141308 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png index 665ffd51f2..0ef6f18ac9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b7f126c54b4e32c57c62ee5d759683ac538d95649f9bd90182e1b2990bb60f7 -size 68021 +oid sha256:1e86235f836e89a9748d68dde6287de5c840a70f39dd76c3f43f1e360cb3abc3 +size 67994 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_17_en.png index 342d1ab6ff..a13655bf74 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5c44832e3d4cbb637d48abc19174a66bdff343de3e60aa94c3d6bfaac44e78d -size 62363 +oid sha256:004f6237e1f07aa2a62930b93e9f45d8ebd4abc7b359c974bb487ad586f60357 +size 62250 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png index 665ffd51f2..0ef6f18ac9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b7f126c54b4e32c57c62ee5d759683ac538d95649f9bd90182e1b2990bb60f7 -size 68021 +oid sha256:1e86235f836e89a9748d68dde6287de5c840a70f39dd76c3f43f1e360cb3abc3 +size 67994 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png index 665ffd51f2..0ef6f18ac9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b7f126c54b4e32c57c62ee5d759683ac538d95649f9bd90182e1b2990bb60f7 -size 68021 +oid sha256:1e86235f836e89a9748d68dde6287de5c840a70f39dd76c3f43f1e360cb3abc3 +size 67994 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png index cfd1827f50..ab727d2957 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6618a3f641c4919fd009f8bab432cd0d03ee933accf5b33815202eed721c78bf -size 65320 +oid sha256:229bdf660691f48123b4d7b0de033032f5450fb24dae2df657672231f7368c7c +size 65167 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png index 665ffd51f2..0ef6f18ac9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b7f126c54b4e32c57c62ee5d759683ac538d95649f9bd90182e1b2990bb60f7 -size 68021 +oid sha256:1e86235f836e89a9748d68dde6287de5c840a70f39dd76c3f43f1e360cb3abc3 +size 67994 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png index 516089368e..5b193f20cd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd658e684c89fc1b201a4a4090037d83c8882774b2a2593b12b30bd011971ecf -size 55324 +oid sha256:c4f07548ec04398f5446d01156decfe01110f41161fb7355207768ab6341c593 +size 55183 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png index f4bb27c8b4..7674a58449 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c5798c49f07cd4a564c6b48c18dc0a67b5437e403da63e154c6bdbcee9fb060 -size 55128 +oid sha256:83e0e84e890b40d4db862ff65689e3234abb205b7aa759bd0c653af47cf0f53e +size 54984 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png index b43498911a..55ff492464 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c8042cfa5d850e0322da8a6cb1fb019da4d3675c469d29aec84fe9d6a8aa147 -size 53368 +oid sha256:c55cff055873cf6301962c164865f728032e08a6ab4c676be84ddddddaadc99e +size 53226 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png index 4b4b7a11cd..7904b5cee7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ca44c782087bba8cb2481318a3bbe902c5c6aafd6dc05b441fb486c6e72f892 -size 62673 +oid sha256:cb4dd312bb803fc72a94aed07063bf0c4f0ca7afb83495062c8c8ab0792d7ad9 +size 62720 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_17_en.png index a2a191b30f..453c5f37b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:513e23929e81026a24cb2d964fd8a65ba4998ac715021d480d7e7211d6a48d0a -size 59148 +oid sha256:54a0b9e36eb0091d7a29d6b88e0e40424259ab04ff3544017ce249367109fd0b +size 58996 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png index 4b4b7a11cd..7904b5cee7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ca44c782087bba8cb2481318a3bbe902c5c6aafd6dc05b441fb486c6e72f892 -size 62673 +oid sha256:cb4dd312bb803fc72a94aed07063bf0c4f0ca7afb83495062c8c8ab0792d7ad9 +size 62720 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png index 4b4b7a11cd..7904b5cee7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ca44c782087bba8cb2481318a3bbe902c5c6aafd6dc05b441fb486c6e72f892 -size 62673 +oid sha256:cb4dd312bb803fc72a94aed07063bf0c4f0ca7afb83495062c8c8ab0792d7ad9 +size 62720 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png index a1d6128daa..87b9d4b528 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89e25edf690c8c416699e82781a4dd01f4ab370e8ca868b7bd4ce1a0e5a1124e -size 60532 +oid sha256:0b6c5ebd0eea3715dc7f053ec196223f64226231f41f7a03251784828a0dd876 +size 60371 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png index 4b4b7a11cd..7904b5cee7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ca44c782087bba8cb2481318a3bbe902c5c6aafd6dc05b441fb486c6e72f892 -size 62673 +oid sha256:cb4dd312bb803fc72a94aed07063bf0c4f0ca7afb83495062c8c8ab0792d7ad9 +size 62720 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png index 14dc407833..f065d6e748 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:784de072b6803569aaba3fddfeb1301ace32f51d2a3b78d823ccaaa77fd5d52e -size 52222 +oid sha256:5b90e992b69d06287ce60300b796da1ac17dddea00922f2e42dfdf63631e8a3e +size 52027 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png index d4f6602825..fc2be0901e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdec8293d351e9b879bafdff048a9ae3bdeaf34d1e47125fe94386415ef80ac4 -size 51941 +oid sha256:bf551434aa4ea85f250fab838aabb77a41b834f8bfdba5cdcd1263e16e67fb39 +size 51759 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png index 2dcffff9f8..c7e8c0a35f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0599ae5bfbd84c1087b06b1e50352fd21b0d784e62536868f012f8a3ab8da95 -size 50257 +oid sha256:95001ba7ba09516aa2da086e07d3dbf2931698f36c9c8425155204e0f8d0bf2d +size 50058 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index a46ae3adc1..c4e53a55c2 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -211,6 +211,7 @@ "banner\\.set_up_recovery\\..*", "banner\\.battery_optimization\\..*", "banner\\.new_sound\\..*", + "banner\\.swipe_avatar\\..*", "full_screen_intent_banner_.*", "screen_migration_.*", "screen_invites_.*"