Merge branch 'develop' into feature/fga/space_list_join_action
This commit is contained in:
commit
0390fde615
591 changed files with 6155 additions and 2140 deletions
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector 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
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
class CurrentUserWithNeighborsBuilder {
|
||||
/**
|
||||
* Build a list of [MatrixUser] containing the current user. If there are other sessions, the list
|
||||
* will contain 3 users, with the current user in the middle.
|
||||
* If there is only one other session, the list will contain twice the other user, to allow cycling.
|
||||
*/
|
||||
fun build(
|
||||
matrixUser: MatrixUser,
|
||||
sessions: List<SessionData>,
|
||||
): ImmutableList<MatrixUser> {
|
||||
// Sort by position to always have the same order (not depending on last account usage)
|
||||
return sessions.sortedBy { it.position }
|
||||
.map {
|
||||
if (it.userId == matrixUser.userId.value) {
|
||||
// Always use the freshest profile for the current user
|
||||
matrixUser
|
||||
} else {
|
||||
// Use the data from the DB
|
||||
MatrixUser(
|
||||
userId = UserId(it.userId),
|
||||
displayName = it.userDisplayName,
|
||||
avatarUrl = it.userAvatarUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
.let { sessionList ->
|
||||
// If the list has one item, there is no other session, return the list
|
||||
when (sessionList.size) {
|
||||
// Can happen when the user signs out (?)
|
||||
0 -> listOf(matrixUser)
|
||||
1 -> sessionList
|
||||
else -> {
|
||||
// Create a list with extra item at the start and end if necessary to have the current user in the middle
|
||||
// If the list is [A, B, C, D] and the current user is A we want to return [D, A, B]
|
||||
// If the current user is B, we want to return [A, B, C]
|
||||
// If the current user is C, we want to return [B, C, D]
|
||||
// If the current user is D, we want to return [C, D, A]
|
||||
// Special case: if there are only two users, we want to return [B, A, B] or [A, B, A] to allows cycling
|
||||
// between the two users.
|
||||
val currentUserIndex = sessionList.indexOfFirst { it.userId == matrixUser.userId }
|
||||
when (currentUserIndex) {
|
||||
// This can happen when the user signs out.
|
||||
// In this case, just return a singleton list with the current user.
|
||||
-1 -> listOf(matrixUser)
|
||||
0 -> listOf(sessionList.last()) + sessionList.take(2)
|
||||
sessionList.lastIndex -> sessionList.takeLast(2) + sessionList.first()
|
||||
else -> sessionList.slice(currentUserIndex - 1..currentUserIndex + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
package io.element.android.features.home.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
sealed interface HomeEvents {
|
||||
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents
|
||||
data class SwitchToAccount(val sessionId: SessionId) : HomeEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
|||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
|
|
@ -56,7 +56,7 @@ import kotlinx.coroutines.withContext
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class HomeFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import androidx.compose.runtime.derivedStateOf
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
|
@ -29,6 +30,10 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||
import io.element.android.libraries.indicator.api.IndicatorService
|
||||
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.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
class HomePresenter(
|
||||
|
|
@ -41,10 +46,21 @@ class HomePresenter(
|
|||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val sessionStore: SessionStore,
|
||||
) : Presenter<HomeState> {
|
||||
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
|
||||
|
||||
@Composable
|
||||
override fun present(): HomeState {
|
||||
val matrixUser = client.userProfile.collectAsState()
|
||||
val coroutineState = rememberCoroutineScope()
|
||||
val matrixUser by client.userProfile.collectAsState()
|
||||
val currentUserAndNeighbors by remember {
|
||||
combine(
|
||||
client.userProfile,
|
||||
sessionStore.sessionsFlow(),
|
||||
currentUserWithNeighborsBuilder::build,
|
||||
)
|
||||
}.collectAsState(initial = persistentListOf(matrixUser))
|
||||
val isOnline by syncService.isOnline.collectAsState()
|
||||
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||
val roomListState = roomListPresenter.present()
|
||||
|
|
@ -71,6 +87,9 @@ class HomePresenter(
|
|||
is HomeEvents.SelectHomeNavigationBarItem -> {
|
||||
currentHomeNavigationBarItemOrdinal = event.item.ordinal
|
||||
}
|
||||
is HomeEvents.SwitchToAccount -> coroutineState.launch {
|
||||
sessionStore.setLatestSession(event.sessionId.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +101,7 @@ class HomePresenter(
|
|||
}
|
||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||
return HomeState(
|
||||
matrixUser = matrixUser.value,
|
||||
currentUserAndNeighbors = currentUserAndNeighbors,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
hasNetworkConnection = isOnline,
|
||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,15 @@ import io.element.android.features.home.impl.spaces.HomeSpacesState
|
|||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class HomeState(
|
||||
val matrixUser: MatrixUser,
|
||||
/**
|
||||
* The current user of this session, in case of multiple accounts, will contains 3 items, with the
|
||||
* current user in the middle.
|
||||
*/
|
||||
val currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||
val showAvatarIndicator: Boolean,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val currentHomeNavigationBarItem: HomeNavigationBarItem,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
open class HomeStateProvider : PreviewParameterProvider<HomeState> {
|
||||
override val values: Sequence<HomeState>
|
||||
|
|
@ -50,6 +51,7 @@ open class HomeStateProvider : PreviewParameterProvider<HomeState> {
|
|||
|
||||
internal fun aHomeState(
|
||||
matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
||||
currentUserAndNeighbors: List<MatrixUser> = listOf(matrixUser),
|
||||
showAvatarIndicator: Boolean = false,
|
||||
hasNetworkConnection: Boolean = true,
|
||||
snackbarMessage: SnackbarMessage? = null,
|
||||
|
|
@ -61,7 +63,7 @@ internal fun aHomeState(
|
|||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||
eventSink: (HomeEvents) -> Unit = {}
|
||||
) = HomeState(
|
||||
matrixUser = matrixUser,
|
||||
currentUserAndNeighbors = currentUserAndNeighbors.toPersistentList(),
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
hasNetworkConnection = hasNetworkConnection,
|
||||
snackbarMessage = snackbarMessage,
|
||||
|
|
|
|||
|
|
@ -171,12 +171,15 @@ private fun HomeScaffold(
|
|||
topBar = {
|
||||
RoomListTopBar(
|
||||
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
|
||||
matrixUser = state.matrixUser,
|
||||
currentUserAndNeighbors = state.currentUserAndNeighbors,
|
||||
showAvatarIndicator = state.showAvatarIndicator,
|
||||
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
|
||||
onToggleSearch = { roomListState.eventSink(RoomListEvents.ToggleSearchResults) },
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onAccountSwitch = {
|
||||
state.eventSink(HomeEvents.SwitchToAccount(it))
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
displayMenuItems = state.displayActions,
|
||||
displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats,
|
||||
|
|
|
|||
|
|
@ -11,19 +11,25 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
|
|
@ -41,7 +47,6 @@ import io.element.android.features.home.impl.filters.RoomListFiltersView
|
|||
import io.element.android.features.home.impl.filters.aRoomListFiltersState
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.modifiers.backgroundVerticalGradient
|
||||
|
|
@ -57,23 +62,29 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomListTopBar(
|
||||
title: String,
|
||||
matrixUser: MatrixUser,
|
||||
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||
showAvatarIndicator: Boolean,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
onToggleSearch: () -> Unit,
|
||||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
onAccountSwitch: (SessionId) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
displayMenuItems: Boolean,
|
||||
displayFilters: Boolean,
|
||||
|
|
@ -83,10 +94,11 @@ fun RoomListTopBar(
|
|||
) {
|
||||
DefaultRoomListTopBar(
|
||||
title = title,
|
||||
matrixUser = matrixUser,
|
||||
currentUserAndNeighbors = currentUserAndNeighbors,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
areSearchResultsDisplayed = areSearchResultsDisplayed,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onAccountSwitch = onAccountSwitch,
|
||||
onSearchClick = onToggleSearch,
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
scrollBehavior = scrollBehavior,
|
||||
|
|
@ -102,11 +114,12 @@ fun RoomListTopBar(
|
|||
@Composable
|
||||
private fun DefaultRoomListTopBar(
|
||||
title: String,
|
||||
matrixUser: MatrixUser,
|
||||
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||
showAvatarIndicator: Boolean,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
onOpenSettings: () -> Unit,
|
||||
onAccountSwitch: (SessionId) -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
displayMenuItems: Boolean,
|
||||
|
|
@ -116,12 +129,6 @@ private fun DefaultRoomListTopBar(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val collapsedFraction = scrollBehavior.state.collapsedFraction
|
||||
val avatarData by remember(matrixUser) {
|
||||
derivedStateOf {
|
||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
val collapsedTitleTextStyle = ElementTheme.typography.aliasScreenTitle
|
||||
val expandedTitleTextStyle = ElementTheme.typography.fontHeadingLgBold.copy(
|
||||
|
|
@ -158,8 +165,9 @@ private fun DefaultRoomListTopBar(
|
|||
},
|
||||
navigationIcon = {
|
||||
NavigationIcon(
|
||||
avatarData = avatarData,
|
||||
currentUserAndNeighbors = currentUserAndNeighbors,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
onAccountSwitch = onAccountSwitch,
|
||||
onClick = onOpenSettings,
|
||||
)
|
||||
},
|
||||
|
|
@ -247,19 +255,67 @@ private fun DefaultRoomListTopBar(
|
|||
|
||||
@Composable
|
||||
private fun NavigationIcon(
|
||||
avatarData: AvatarData,
|
||||
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||
showAvatarIndicator: Boolean,
|
||||
onAccountSwitch: (SessionId) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
if (currentUserAndNeighbors.size == 1) {
|
||||
AccountIcon(
|
||||
matrixUser = currentUserAndNeighbors.single(),
|
||||
isCurrentAccount = true,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
// Render a vertical pager
|
||||
val pagerState = rememberPagerState(initialPage = 1) { currentUserAndNeighbors.size }
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.height(48.dp),
|
||||
) { page ->
|
||||
AccountIcon(
|
||||
matrixUser = currentUserAndNeighbors[page],
|
||||
isCurrentAccount = page == 1,
|
||||
showAvatarIndicator = page == 1 && showAvatarIndicator,
|
||||
onClick = if (page == 1) {
|
||||
onClick
|
||||
} else {
|
||||
{}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountIcon(
|
||||
matrixUser: MatrixUser,
|
||||
isCurrentAccount: Boolean,
|
||||
showAvatarIndicator: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||
modifier = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier,
|
||||
onClick = onClick,
|
||||
) {
|
||||
Box {
|
||||
val avatarData by remember(matrixUser) {
|
||||
derivedStateOf {
|
||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
contentDescription = stringResource(CommonStrings.common_settings),
|
||||
contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null,
|
||||
)
|
||||
if (showAvatarIndicator) {
|
||||
RedIndicatorAtom(
|
||||
|
|
@ -276,11 +332,12 @@ private fun NavigationIcon(
|
|||
internal fun DefaultRoomListTopBarPreview() = ElementPreview {
|
||||
DefaultRoomListTopBar(
|
||||
title = stringResource(R.string.screen_roomlist_main_space_title),
|
||||
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
||||
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
||||
showAvatarIndicator = false,
|
||||
areSearchResultsDisplayed = false,
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onAccountSwitch = {},
|
||||
onSearchClick = {},
|
||||
displayMenuItems = true,
|
||||
displayFilters = true,
|
||||
|
|
@ -296,11 +353,33 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
|
|||
internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
|
||||
DefaultRoomListTopBar(
|
||||
title = stringResource(R.string.screen_roomlist_main_space_title),
|
||||
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
||||
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
||||
showAvatarIndicator = true,
|
||||
areSearchResultsDisplayed = false,
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onAccountSwitch = {},
|
||||
onSearchClick = {},
|
||||
displayMenuItems = true,
|
||||
displayFilters = true,
|
||||
filtersState = aRoomListFiltersState(),
|
||||
canReportBug = true,
|
||||
onMenuActionClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun DefaultRoomListTopBarMultiAccountPreview() = ElementPreview {
|
||||
DefaultRoomListTopBar(
|
||||
title = stringResource(R.string.screen_roomlist_main_space_title),
|
||||
currentUserAndNeighbors = aMatrixUserList().take(3).toPersistentList(),
|
||||
showAvatarIndicator = false,
|
||||
areSearchResultsDisplayed = false,
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onAccountSwitch = {},
|
||||
onSearchClick = {},
|
||||
displayMenuItems = true,
|
||||
displayFilters = true,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@
|
|||
<string name="screen_home_tab_chats">"Всички чатове"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Отказване на покана"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Сигурни ли сте, че искате да откажете този личен чат с %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Отказване на чат"</string>
|
||||
<string name="screen_invites_empty_list">"Няма покани"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) ви покани"</string>
|
||||
<string name="screen_migration_message">"Това е еднократен процес, благодаря, че изчакахте."</string>
|
||||
<string name="screen_migration_title">"Настройване на вашия акаунт."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Създаване на нов разговор или стая"</string>
|
||||
<string name="screen_roomlist_empty_message">"Започнете, като изпратите съобщение на някого."</string>
|
||||
<string name="screen_roomlist_empty_title">"Все още няма чатове."</string>
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@
|
|||
<string name="confirm_recovery_key_banner_primary_button_title">"Enter your backup password"</string>
|
||||
<string name="confirm_recovery_key_banner_secondary_button_title">"Forgot your backup password?"</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Your message backup is out of sync"</string>
|
||||
<string name="session_verification_banner_message">"Looks like you\'re using a new device. Confirm it with another connected device to access your encrypted messages."</string>
|
||||
<string name="session_verification_banner_message">"Looks like you\'re using a new device. Confirm it with another linked device to access your encrypted messages."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_battery_optimization_content_android">"Kapcsolja ki az alkalmazás akkumulátor-optimalizálását, hogy biztosan megkapja az összes értesítést."</string>
|
||||
<string name="banner_battery_optimization_content_android">"Kapcsolja ki az alkalmazás akkumulátoroptimalizálását, hogy biztosan megkapja az összes értesítést."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"Optimalizálás letiltása"</string>
|
||||
<string name="banner_battery_optimization_title_android">"Nem érkeznek meg az értesítések?"</string>
|
||||
<string name="banner_set_up_recovery_content">"Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector 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
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import org.junit.Test
|
||||
|
||||
class CurrentUserWithNeighborsBuilderTest {
|
||||
@Test
|
||||
fun `build on empty list returns current user`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser()
|
||||
val list = listOf<SessionData>()
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result).containsExactly(matrixUser)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure that account are sorted by position`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
position = 3,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
position = 2,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
position = 1,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID_3,
|
||||
A_USER_ID_2,
|
||||
A_USER_ID,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if current user is not found, return a singleton with current user`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one account, will return a singleton`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two accounts, first is current, will return 3 items`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID_2,
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two accounts, second is current, will return 3 items`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
A_USER_ID,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `three accounts, first is current, will return last current and next`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID_3,
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `three accounts, second is current, will return first current and last`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
A_USER_ID_3,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `three accounts, current is last, will return middle, current and first`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID_3.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
A_USER_ID_3,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one account, will return data from matrix user and not from db`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(
|
||||
id = A_USER_ID.value,
|
||||
displayName = "Bob",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
userDisplayName = "Outdated Bob",
|
||||
userAvatarUrl = "outdatedAvatarUrl",
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result).containsExactly(
|
||||
MatrixUser(
|
||||
userId = A_USER_ID,
|
||||
displayName = "Bob",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,9 @@ 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
|
||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||
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.tests.testutils.MutablePresenter
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
|
|
@ -54,17 +57,29 @@ class HomePresenterTest {
|
|||
val presenter = createHomePresenter(
|
||||
client = matrixClient,
|
||||
rageshakeFeatureAvailability = { flowOf(false) },
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = matrixClient.sessionId.value,
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID))
|
||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(
|
||||
MatrixUser(A_USER_ID, null, null)
|
||||
)
|
||||
assertThat(initialState.canReportBug).isFalse()
|
||||
skipItems(1)
|
||||
val withUserState = awaitItem()
|
||||
assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID)
|
||||
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
|
||||
assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
assertThat(withUserState.currentUserAndNeighbors.first()).isEqualTo(
|
||||
MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
|
||||
)
|
||||
assertThat(withUserState.showAvatarIndicator).isFalse()
|
||||
assertThat(withUserState.isSpaceFeatureEnabled).isFalse()
|
||||
assertThat(withUserState.showNavigationBar).isFalse()
|
||||
|
|
@ -75,6 +90,9 @@ class HomePresenterTest {
|
|||
fun `present - can report bug`() = runTest {
|
||||
val presenter = createHomePresenter(
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -92,6 +110,9 @@ class HomePresenterTest {
|
|||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.Space.key to true),
|
||||
),
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
|
|
@ -105,6 +126,9 @@ class HomePresenterTest {
|
|||
val indicatorService = FakeIndicatorService()
|
||||
val presenter = createHomePresenter(
|
||||
indicatorService = indicatorService,
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -124,19 +148,28 @@ class HomePresenterTest {
|
|||
userAvatarUrl = null,
|
||||
)
|
||||
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION))
|
||||
val presenter = createHomePresenter(client = matrixClient)
|
||||
val presenter = createHomePresenter(
|
||||
client = matrixClient,
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId))
|
||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId))
|
||||
// No new state is coming
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - NavigationBar change`() = runTest {
|
||||
val presenter = createHomePresenter()
|
||||
val presenter = createHomePresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -152,6 +185,9 @@ class HomePresenterTest {
|
|||
fun `present - NavigationBar is hidden when the last space is left`() = runTest {
|
||||
val homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
|
||||
val presenter = createHomePresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.Space.key to true),
|
||||
),
|
||||
|
|
@ -185,6 +221,7 @@ internal fun createHomePresenter(
|
|||
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
) = HomePresenter(
|
||||
client = client,
|
||||
syncService = syncService,
|
||||
|
|
@ -195,4 +232,5 @@ internal fun createHomePresenter(
|
|||
homeSpacesPresenter = homeSpacesPresenter,
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
featureFlagService = featureFlagService,
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue