mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Merge branch 'develop' into feature/bma/swipeAvatarAnnouncement
Account switching is now handled by RootFlowNode, through the onSwitchToAccount callback, instead of HomeEvent.SwitchToAccount. To keep dismissing the swipe avatar announcement when the user switches account by swiping their avatar, HomeView now also sends the new HomeEvent.DismissSwipeAvatarAnnouncement. Also opt in to ExperimentalCoroutinesApi in the HomePresenter tests using advanceTimeBy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-3
@@ -8,10 +8,8 @@
|
||||
|
||||
package io.element.android.features.home.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
sealed interface HomeEvent {
|
||||
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvent
|
||||
data class SwitchToAccount(val sessionId: SessionId) : HomeEvent
|
||||
data object DismissMultiAccountAnnouncement : HomeEvent
|
||||
data object DismissSwipeAvatarAnnouncement : HomeEvent
|
||||
}
|
||||
|
||||
+1
@@ -222,6 +222,7 @@ class HomeFlowNode(
|
||||
onRoomClick = ::navigateToRoom,
|
||||
onSettingsClick = callback::navigateToSettings,
|
||||
onAddAccountClick = callback::navigateToAddAccount,
|
||||
onSwitchToAccount = callback::navigateToSession,
|
||||
onStartChatClick = callback::navigateToCreateRoom,
|
||||
onCreateSpaceClick = callback::navigateToCreateSpace,
|
||||
onSetUpRecoveryClick = callback::navigateToSetUpRecovery,
|
||||
|
||||
+18
-4
@@ -14,6 +14,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@@ -34,9 +35,14 @@ 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.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Inject
|
||||
class HomePresenter(
|
||||
@@ -53,10 +59,20 @@ class HomePresenter(
|
||||
) : Presenter<HomeState> {
|
||||
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
override fun present(): HomeState {
|
||||
val coroutineState = rememberCoroutineScope()
|
||||
val matrixUser by client.userProfile.collectAsState()
|
||||
|
||||
val totalUnreadCount by produceState(client.totalUnreadCount().getOrDefault(0).toInt()) {
|
||||
syncService.heartBeat
|
||||
// Delay the heartbeat so the SDK has time to update the unread count before we read it.
|
||||
.debounce(100.milliseconds)
|
||||
.mapLatest { client.totalUnreadCount().getOrDefault(0).toInt() }
|
||||
.collect { value = it }
|
||||
}
|
||||
|
||||
val currentUserAndNeighbors by remember {
|
||||
combine(
|
||||
client.userProfile,
|
||||
@@ -110,11 +126,8 @@ class HomePresenter(
|
||||
is HomeEvent.SelectHomeNavigationBarItem -> {
|
||||
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.
|
||||
HomeEvent.DismissSwipeAvatarAnnouncement -> coroutineState.launch {
|
||||
announcementService.onAnnouncementDismissed(Announcement.SwipeAvatarToSwitchAccount)
|
||||
sessionStore.setLatestSession(event.sessionId.value)
|
||||
}
|
||||
HomeEvent.DismissMultiAccountAnnouncement -> coroutineState.launch {
|
||||
announcementService.onAnnouncementDismissed(Announcement.MultiAccount)
|
||||
@@ -133,6 +146,7 @@ class HomePresenter(
|
||||
snackbarMessage = snackbarMessage,
|
||||
canReportBug = canReportBug,
|
||||
showMultiAccountAnnouncement = showMultiAccountAnnouncement,
|
||||
totalUnreadCount = totalUnreadCount,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ data class HomeState(
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val canReportBug: Boolean,
|
||||
val showMultiAccountAnnouncement: Boolean,
|
||||
val totalUnreadCount: Int,
|
||||
val eventSink: (HomeEvent) -> Unit,
|
||||
) {
|
||||
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
|
||||
|
||||
+2
@@ -64,6 +64,7 @@ internal fun aHomeState(
|
||||
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
||||
canReportBug: Boolean = true,
|
||||
showMultiAccountAnnouncement: Boolean = false,
|
||||
totalUnreadCount: Int = 0,
|
||||
eventSink: (HomeEvent) -> Unit = {}
|
||||
) = HomeState(
|
||||
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
|
||||
@@ -75,5 +76,6 @@ internal fun aHomeState(
|
||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||
roomListState = roomListState,
|
||||
homeSpacesState = homeSpacesState,
|
||||
totalUnreadCount = totalUnreadCount,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
+13
-2
@@ -69,6 +69,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -78,6 +79,7 @@ fun HomeView(
|
||||
onRoomClick: (RoomId, EventId?) -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onAddAccountClick: () -> Unit,
|
||||
onSwitchToAccount: (SessionId) -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onStartChatClick: () -> Unit,
|
||||
@@ -130,6 +132,7 @@ fun HomeView(
|
||||
onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
|
||||
onCreateSpaceClick = { if (firstThrottler.canHandle()) onCreateSpaceClick() },
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
onSwitchToAccount = onSwitchToAccount,
|
||||
)
|
||||
|
||||
if (state.globalSearchState.isEnabled) {
|
||||
@@ -167,6 +170,7 @@ private fun HomeScaffold(
|
||||
onStartChatClick: () -> Unit,
|
||||
onCreateSpaceClick: () -> Unit,
|
||||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
onSwitchToAccount: (SessionId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onRoomClick(room: RoomListRoomSummary) {
|
||||
@@ -213,8 +217,10 @@ private fun HomeScaffold(
|
||||
},
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onAccountSwitch = {
|
||||
state.eventSink(HomeEvent.SwitchToAccount(it))
|
||||
onAccountSwitch = { sessionId ->
|
||||
// The user has switched account by swiping their avatar, so they know the gesture.
|
||||
state.eventSink(HomeEvent.DismissSwipeAvatarAnnouncement)
|
||||
onSwitchToAccount(sessionId)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
displayFilters = state.displayRoomListFilters,
|
||||
@@ -259,6 +265,7 @@ private fun HomeScaffold(
|
||||
}
|
||||
}
|
||||
},
|
||||
totalUnreadCount = state.totalUnreadCount,
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
@@ -341,6 +348,7 @@ private fun HomeFloatingActionButton(
|
||||
|
||||
@Composable
|
||||
private fun HomeBottomBar(
|
||||
totalUnreadCount: Int,
|
||||
currentHomeNavigationBarItem: HomeNavigationBarItem,
|
||||
onItemClick: (HomeNavigationBarItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -361,6 +369,7 @@ private fun HomeBottomBar(
|
||||
tooltipLabel = stringResource(item.labelRes),
|
||||
isSelected = isSelected,
|
||||
onClick = { onItemClick(item) },
|
||||
counter = totalUnreadCount.takeIf { item == HomeNavigationBarItem.Chats && it > 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -376,6 +385,7 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStatePreviewParam::class) sta
|
||||
onRoomClick = { _, _ -> },
|
||||
onSettingsClick = {},
|
||||
onAddAccountClick = {},
|
||||
onSwitchToAccount = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onStartChatClick = {},
|
||||
@@ -397,6 +407,7 @@ internal fun HomeViewA11yPreview() = ElementPreview {
|
||||
onRoomClick = { _, _ -> },
|
||||
onSettingsClick = {},
|
||||
onAddAccountClick = {},
|
||||
onSwitchToAccount = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onStartChatClick = {},
|
||||
|
||||
+2
@@ -14,6 +14,7 @@ import io.element.android.features.home.api.HomeEntryPoint
|
||||
import io.element.android.features.invite.test.declineandblock.FakeDeclineInviteAndBlockEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
@@ -53,6 +54,7 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
|
||||
override fun navigateToRoomSettings(roomId: RoomId) = lambdaError()
|
||||
override fun navigateToBugReport() = lambdaError()
|
||||
override fun navigateToAddAccount() = lambdaError()
|
||||
override fun navigateToSession(sessionId: SessionId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
|
||||
+48
-16
@@ -28,8 +28,6 @@ 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
|
||||
@@ -43,10 +41,12 @@ 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.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class HomePresenterTest {
|
||||
@get:Rule
|
||||
@@ -121,6 +121,40 @@ class HomePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - total unread count is updated on sync heartbeat`() = runTest {
|
||||
var unreadCount = 0L
|
||||
val syncService = FakeSyncService()
|
||||
val presenter = createHomePresenter(
|
||||
client = FakeMatrixClient(totalUnreadCountResult = { Result.success(unreadCount) }),
|
||||
syncService = syncService,
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().totalUnreadCount).isEqualTo(0)
|
||||
unreadCount = 5
|
||||
syncService.emitHeartBeat()
|
||||
advanceTimeBy(200.milliseconds)
|
||||
assertThat(awaitItem().totalUnreadCount).isEqualTo(5)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - total unread count defaults to 0 when the SDK call fails`() = runTest {
|
||||
val syncService = FakeSyncService()
|
||||
val presenter = createHomePresenter(
|
||||
client = FakeMatrixClient(totalUnreadCountResult = { Result.failure(AN_EXCEPTION) }),
|
||||
syncService = syncService,
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().totalUnreadCount).isEqualTo(0)
|
||||
syncService.emitHeartBeat()
|
||||
advanceTimeBy(200.milliseconds)
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - should start with no user and then load user with error`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
@@ -252,31 +286,29 @@ class HomePresenterTest {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - switching account dismisses the swipe avatar announcement`() = runTest {
|
||||
fun `present - dismissing 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))
|
||||
state.eventSink(HomeEvent.DismissSwipeAvatarAnnouncement)
|
||||
runCurrent()
|
||||
onAnnouncementDismissedResult.assertions().isCalledOnce()
|
||||
.with(value(Announcement.SwipeAvatarToSwitchAccount))
|
||||
setLatestSessionResult.assertions().isCalledOnce()
|
||||
.with(value(A_SESSION_ID_2.value))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - total unread count is loaded in the initial state`() = runTest {
|
||||
val presenter = createHomePresenter(
|
||||
client = FakeMatrixClient(totalUnreadCountResult = { Result.success(5) }),
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().totalUnreadCount).isEqualTo(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -331,6 +331,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setRoomListView(
|
||||
onRoomClick = { roomId, _ -> onRoomClick(roomId) },
|
||||
onSettingsClick = onSettingsClick,
|
||||
onAddAccountClick = onAddAccountClick,
|
||||
onSwitchToAccount = EnsureNeverCalledWithParam(),
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onStartChatClick = onCreateRoomClick,
|
||||
|
||||
Reference in New Issue
Block a user