mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Announce the swipe gesture on the account avatar
Display a banner on the room list when 2 or more accounts are logged in, to let the user know that they can swipe their avatar to switch account. The banner is stored as a new Announcement.SwipeAvatarToSwitchAccount, and is dismissed when the user taps on it, or when the user switches account by swiping their avatar. The banner is displayed at the same place as the new notification sound banner, but never at the same time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
@@ -19,4 +19,6 @@ sealed interface Announcement {
|
||||
data object NewNotificationSound : Announcement
|
||||
|
||||
data object MultiAccount : Announcement
|
||||
|
||||
data object SwipeAvatarToSwitchAccount : Announcement
|
||||
}
|
||||
|
||||
+7
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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
|
||||
}
|
||||
|
||||
+23
-5
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -111,6 +111,9 @@ class HomePresenter(
|
||||
currentHomeNavigationBarItemOrdinal = event.item.ordinal
|
||||
}
|
||||
is HomeEvent.SwitchToAccount -> coroutineState.launch {
|
||||
// The user has switched account by swiping their avatar, so they know the gesture.
|
||||
// Do it before switching, since it can stop this coroutine.
|
||||
announcementService.onAnnouncementDismissed(Announcement.SwipeAvatarToSwitchAccount)
|
||||
sessionStore.setLatestSession(event.sessionId.value)
|
||||
}
|
||||
HomeEvent.DismissMultiAccountAnnouncement -> coroutineState.launch {
|
||||
|
||||
+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,
|
||||
|
||||
+8
-1
@@ -253,7 +253,7 @@ private fun RoomsViewList(
|
||||
}
|
||||
}
|
||||
// Banner precedence (top-to-bottom): full-screen-intent > battery-optimization >
|
||||
// new-notification-sound > sound-unavailable. At most one renders at a time.
|
||||
// new-notification-sound > swipe-avatar > sound-unavailable. At most one renders at a time.
|
||||
SecurityBannerState.None -> when {
|
||||
state.fullScreenIntentPermissionsState.shouldDisplayBanner -> {
|
||||
item {
|
||||
@@ -272,6 +272,13 @@ private fun RoomsViewList(
|
||||
)
|
||||
}
|
||||
}
|
||||
state.showSwipeAvatarBanner -> {
|
||||
item {
|
||||
SwipeAvatarBanner(
|
||||
onDismissClick = { eventSink(RoomListEvent.DismissSwipeAvatarBanner) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+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 = {},
|
||||
)
|
||||
}
|
||||
+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>,
|
||||
|
||||
+34
@@ -28,6 +28,8 @@ import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
@@ -39,7 +41,9 @@ 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.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -245,6 +249,36 @@ class HomePresenterTest {
|
||||
.with(value(Announcement.MultiAccount))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - switching account dismisses the swipe avatar announcement`() = runTest {
|
||||
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
||||
val setLatestSessionResult = lambdaRecorder<String, Unit> { }
|
||||
val presenter = createHomePresenter(
|
||||
client = FakeMatrixClient(sessionId = A_SESSION_ID),
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(sessionId = A_SESSION_ID.value),
|
||||
aSessionData(sessionId = A_SESSION_ID_2.value),
|
||||
),
|
||||
updateUserProfileResult = { _, _, _, _ -> },
|
||||
setLatestSessionResult = setLatestSessionResult,
|
||||
),
|
||||
announcementService = FakeAnnouncementService(
|
||||
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
val state = expectMostRecentItem()
|
||||
state.eventSink(HomeEvent.SwitchToAccount(A_SESSION_ID_2))
|
||||
runCurrent()
|
||||
onAnnouncementDismissedResult.assertions().isCalledOnce()
|
||||
.with(value(Announcement.SwipeAvatarToSwitchAccount))
|
||||
setLatestSessionResult.assertions().isCalledOnce()
|
||||
.with(value(A_SESSION_ID_2.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createHomePresenter(
|
||||
|
||||
+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,
|
||||
)
|
||||
}
|
||||
|
||||
+21
@@ -94,6 +94,27 @@ class RoomListViewTest : RobolectricTest() {
|
||||
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
|
||||
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>()
|
||||
|
||||
Reference in New Issue
Block a user