Merge pull request #6602 from element-hq/feature/bma/updateSettingsUI
Settings UI update.
This commit is contained in:
commit
2858fb96dd
44 changed files with 781 additions and 290 deletions
|
|
@ -31,7 +31,6 @@ import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
|
|
@ -177,7 +176,6 @@ class LoggedInPresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.preloadAccountManagementUrl() = launch {
|
private fun CoroutineScope.preloadAccountManagementUrl() = launch {
|
||||||
matrixClient.getAccountManagementUrl(AccountManagementAction.Profile)
|
matrixClient.getAccountManagementUrl(null)
|
||||||
matrixClient.getAccountManagementUrl(AccountManagementAction.DevicesList)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ class LoggedInPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - ensure that account urls are preloaded`() = runTest {
|
fun `present - ensure that account url is preloaded`() = runTest {
|
||||||
val accountManagementUrlResult = lambdaRecorder<AccountManagementAction?, Result<String?>> { Result.success("aUrl") }
|
val accountManagementUrlResult = lambdaRecorder<AccountManagementAction?, Result<String?>> { Result.success("aUrl") }
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
accountManagementUrlResult = accountManagementUrlResult,
|
accountManagementUrlResult = accountManagementUrlResult,
|
||||||
|
|
@ -81,11 +81,8 @@ class LoggedInPresenterTest {
|
||||||
).test {
|
).test {
|
||||||
awaitItem()
|
awaitItem()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
accountManagementUrlResult.assertions().isCalledExactly(2)
|
accountManagementUrlResult.assertions().isCalledOnce()
|
||||||
.withSequence(
|
.with(value(null))
|
||||||
listOf(value(AccountManagementAction.Profile)),
|
|
||||||
listOf(value(AccountManagementAction.DevicesList)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
sealed interface PreferencesRootEvents {
|
sealed interface PreferencesRootEvent {
|
||||||
data object OnVersionInfoClick : PreferencesRootEvents
|
data object OnVersionInfoClick : PreferencesRootEvent
|
||||||
data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvents
|
data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvent
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +30,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.indicator.api.IndicatorService
|
import io.element.android.libraries.indicator.api.IndicatorService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
|
@ -99,9 +98,6 @@ class PreferencesRootPresenter(
|
||||||
val accountManagementUrl: MutableState<String?> = remember {
|
val accountManagementUrl: MutableState<String?> = remember {
|
||||||
mutableStateOf(null)
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
val devicesManagementUrl: MutableState<String?> = remember {
|
|
||||||
mutableStateOf(null)
|
|
||||||
}
|
|
||||||
var canDeactivateAccount by remember {
|
var canDeactivateAccount by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
@ -110,9 +106,9 @@ class PreferencesRootPresenter(
|
||||||
canDeactivateAccount = matrixClient.canDeactivateAccount()
|
canDeactivateAccount = matrixClient.canDeactivateAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
val showBlockedUsersItem by produceState(initialValue = false) {
|
val nbOfBlockedUsers by produceState(initialValue = 0) {
|
||||||
matrixClient.ignoredUsersFlow
|
matrixClient.ignoredUsersFlow
|
||||||
.onEach { value = it.isNotEmpty() }
|
.onEach { value = it.size }
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,17 +117,17 @@ class PreferencesRootPresenter(
|
||||||
val directLogoutState = directLogoutPresenter.present()
|
val directLogoutState = directLogoutPresenter.present()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
|
initAccountManagementUrl(accountManagementUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
val showDeveloperSettings by showDeveloperSettingsProvider.showDeveloperSettings.collectAsState()
|
val showDeveloperSettings by showDeveloperSettingsProvider.showDeveloperSettings.collectAsState()
|
||||||
|
|
||||||
fun handleEvent(event: PreferencesRootEvents) {
|
fun handleEvent(event: PreferencesRootEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is PreferencesRootEvents.OnVersionInfoClick -> {
|
is PreferencesRootEvent.OnVersionInfoClick -> {
|
||||||
showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope)
|
showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope)
|
||||||
}
|
}
|
||||||
is PreferencesRootEvents.SwitchToSession -> coroutineScope.launch {
|
is PreferencesRootEvent.SwitchToSession -> coroutineScope.launch {
|
||||||
sessionStore.setLatestSession(event.sessionId.value)
|
sessionStore.setLatestSession(event.sessionId.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -146,13 +142,12 @@ class PreferencesRootPresenter(
|
||||||
showSecureBackup = !canVerifyUserSession,
|
showSecureBackup = !canVerifyUserSession,
|
||||||
showSecureBackupBadge = showSecureBackupIndicator,
|
showSecureBackupBadge = showSecureBackupIndicator,
|
||||||
accountManagementUrl = accountManagementUrl.value,
|
accountManagementUrl = accountManagementUrl.value,
|
||||||
devicesManagementUrl = devicesManagementUrl.value,
|
|
||||||
showAnalyticsSettings = hasAnalyticsProviders,
|
showAnalyticsSettings = hasAnalyticsProviders,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
showLinkNewDevice = showLinkNewDevice,
|
showLinkNewDevice = showLinkNewDevice,
|
||||||
showDeveloperSettings = showDeveloperSettings,
|
showDeveloperSettings = showDeveloperSettings,
|
||||||
canDeactivateAccount = canDeactivateAccount,
|
canDeactivateAccount = canDeactivateAccount,
|
||||||
showBlockedUsersItem = showBlockedUsersItem,
|
nbOfBlockedUsers = nbOfBlockedUsers,
|
||||||
showLabsItem = showLabsItem,
|
showLabsItem = showLabsItem,
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
|
|
@ -162,9 +157,7 @@ class PreferencesRootPresenter(
|
||||||
|
|
||||||
private fun CoroutineScope.initAccountManagementUrl(
|
private fun CoroutineScope.initAccountManagementUrl(
|
||||||
accountManagementUrl: MutableState<String?>,
|
accountManagementUrl: MutableState<String?>,
|
||||||
devicesManagementUrl: MutableState<String?>,
|
|
||||||
) = launch {
|
) = launch {
|
||||||
accountManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.Profile).getOrNull()
|
accountManagementUrl.value = matrixClient.getAccountManagementUrl(null).getOrNull()
|
||||||
devicesManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.DevicesList).getOrNull()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,16 @@ data class PreferencesRootState(
|
||||||
val showSecureBackup: Boolean,
|
val showSecureBackup: Boolean,
|
||||||
val showSecureBackupBadge: Boolean,
|
val showSecureBackupBadge: Boolean,
|
||||||
val accountManagementUrl: String?,
|
val accountManagementUrl: String?,
|
||||||
val devicesManagementUrl: String?,
|
|
||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
val showLinkNewDevice: Boolean,
|
val showLinkNewDevice: Boolean,
|
||||||
val showAnalyticsSettings: Boolean,
|
val showAnalyticsSettings: Boolean,
|
||||||
val showDeveloperSettings: Boolean,
|
val showDeveloperSettings: Boolean,
|
||||||
val canDeactivateAccount: Boolean,
|
val canDeactivateAccount: Boolean,
|
||||||
val showBlockedUsersItem: Boolean,
|
val nbOfBlockedUsers: Int,
|
||||||
val showLabsItem: Boolean,
|
val showLabsItem: Boolean,
|
||||||
val directLogoutState: DirectLogoutState,
|
val directLogoutState: DirectLogoutState,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val eventSink: (PreferencesRootEvents) -> Unit,
|
val eventSink: (PreferencesRootEvent) -> Unit,
|
||||||
)
|
) {
|
||||||
|
val showBlockedUsersItem = nbOfBlockedUsers > 0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,36 +8,103 @@
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.root
|
package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
open class PreferencesRootStateProvider : PreviewParameterProvider<PreferencesRootState> {
|
||||||
|
override val values: Sequence<PreferencesRootState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
// Nominal state, that a regular user will see if multi account is enabled
|
||||||
|
aPreferencesRootState(
|
||||||
|
myUser = aMatrixUser(avatarUrl = "anAvatarUrl"),
|
||||||
|
version = "Version 1.1 (1)",
|
||||||
|
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||||
|
isMultiAccountEnabled = true,
|
||||||
|
otherSessions = aMatrixUserList().drop(1).take(1),
|
||||||
|
showSecureBackup = true,
|
||||||
|
accountManagementUrl = "aUrl",
|
||||||
|
canReportBug = true,
|
||||||
|
showLinkNewDevice = true,
|
||||||
|
showAnalyticsSettings = true,
|
||||||
|
canDeactivateAccount = false,
|
||||||
|
nbOfBlockedUsers = 3,
|
||||||
|
showLabsItem = true,
|
||||||
|
),
|
||||||
|
aPreferencesRootState(
|
||||||
|
myUser = aMatrixUser(displayName = null),
|
||||||
|
isMultiAccountEnabled = true,
|
||||||
|
showSecureBackup = true,
|
||||||
|
canDeactivateAccount = true,
|
||||||
|
),
|
||||||
|
aPreferencesRootState(
|
||||||
|
isMultiAccountEnabled = true,
|
||||||
|
otherSessions = aMatrixUserList().drop(1).take(3),
|
||||||
|
accountManagementUrl = "aUrl",
|
||||||
|
showSecureBackup = true,
|
||||||
|
showSecureBackupBadge = true,
|
||||||
|
),
|
||||||
|
aPreferencesRootState(
|
||||||
|
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||||
|
showLabsItem = true,
|
||||||
|
canReportBug = true,
|
||||||
|
nbOfBlockedUsers = 3,
|
||||||
|
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
||||||
|
),
|
||||||
|
aPreferencesRootState(
|
||||||
|
showLinkNewDevice = true,
|
||||||
|
showAnalyticsSettings = true,
|
||||||
|
showDeveloperSettings = true,
|
||||||
|
canDeactivateAccount = true,
|
||||||
|
),
|
||||||
|
// Minimal state
|
||||||
|
aPreferencesRootState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun aPreferencesRootState(
|
fun aPreferencesRootState(
|
||||||
myUser: MatrixUser = aMatrixUser(),
|
myUser: MatrixUser = aMatrixUser(),
|
||||||
|
version: String = "Version 1.1 (1)",
|
||||||
|
deviceId: DeviceId? = null,
|
||||||
|
isMultiAccountEnabled: Boolean = false,
|
||||||
otherSessions: List<MatrixUser> = emptyList(),
|
otherSessions: List<MatrixUser> = emptyList(),
|
||||||
eventSink: (PreferencesRootEvents) -> Unit = { _ -> },
|
showSecureBackup: Boolean = false,
|
||||||
|
showSecureBackupBadge: Boolean = false,
|
||||||
|
accountManagementUrl: String? = null,
|
||||||
|
canReportBug: Boolean = false,
|
||||||
|
showLinkNewDevice: Boolean = false,
|
||||||
|
showAnalyticsSettings: Boolean = false,
|
||||||
|
showDeveloperSettings: Boolean = false,
|
||||||
|
canDeactivateAccount: Boolean = false,
|
||||||
|
nbOfBlockedUsers: Int = 0,
|
||||||
|
showLabsItem: Boolean = false,
|
||||||
|
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||||
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
|
eventSink: (PreferencesRootEvent) -> Unit = {},
|
||||||
) = PreferencesRootState(
|
) = PreferencesRootState(
|
||||||
myUser = myUser,
|
myUser = myUser,
|
||||||
version = "Version 1.1 (1)",
|
version = version,
|
||||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
deviceId = deviceId,
|
||||||
isMultiAccountEnabled = true,
|
isMultiAccountEnabled = isMultiAccountEnabled,
|
||||||
otherSessions = otherSessions.toImmutableList(),
|
otherSessions = otherSessions.toImmutableList(),
|
||||||
showSecureBackup = true,
|
showSecureBackup = showSecureBackup,
|
||||||
showSecureBackupBadge = true,
|
showSecureBackupBadge = showSecureBackupBadge,
|
||||||
accountManagementUrl = "aUrl",
|
accountManagementUrl = accountManagementUrl,
|
||||||
devicesManagementUrl = "anOtherUrl",
|
canReportBug = canReportBug,
|
||||||
showAnalyticsSettings = true,
|
showLinkNewDevice = showLinkNewDevice,
|
||||||
showLinkNewDevice = true,
|
showAnalyticsSettings = showAnalyticsSettings,
|
||||||
canReportBug = true,
|
showDeveloperSettings = showDeveloperSettings,
|
||||||
showDeveloperSettings = true,
|
canDeactivateAccount = canDeactivateAccount,
|
||||||
showBlockedUsersItem = true,
|
nbOfBlockedUsers = nbOfBlockedUsers,
|
||||||
showLabsItem = true,
|
showLabsItem = showLabsItem,
|
||||||
canDeactivateAccount = true,
|
directLogoutState = directLogoutState,
|
||||||
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
snackbarMessage = snackbarMessage,
|
||||||
directLogoutState = aDirectLogoutState(),
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
package io.element.android.features.preferences.impl.root
|
package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -28,23 +27,20 @@ import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserProvider
|
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -82,22 +78,17 @@ fun PreferencesRootView(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onOpenUserProfile(state.myUser)
|
onOpenUserProfile(state.myUser)
|
||||||
},
|
},
|
||||||
user = state.myUser,
|
matrixUser = state.myUser,
|
||||||
)
|
)
|
||||||
if (state.isMultiAccountEnabled) {
|
if (state.isMultiAccountEnabled) {
|
||||||
MultiAccountSection(
|
MultiAccountSection(
|
||||||
state = state,
|
state = state,
|
||||||
onAddAccountClick = onAddAccountClick,
|
onAddAccountClick = onAddAccountClick,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
// 'Manage my app' section
|
// User status will be added here
|
||||||
ManageAppSection(
|
|
||||||
state = state,
|
|
||||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
|
||||||
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
|
||||||
onSecureBackupClick = onSecureBackupClick,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 'Account' section
|
// 'Account' section
|
||||||
ManageAccountSection(
|
ManageAccountSection(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
@ -105,6 +96,13 @@ fun PreferencesRootView(
|
||||||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||||
onOpenBlockedUsers = onOpenBlockedUsers
|
onOpenBlockedUsers = onOpenBlockedUsers
|
||||||
)
|
)
|
||||||
|
// 'Manage my app' section
|
||||||
|
ManageAppSection(
|
||||||
|
state = state,
|
||||||
|
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||||
|
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||||
|
onSecureBackupClick = onSecureBackupClick,
|
||||||
|
)
|
||||||
|
|
||||||
// General section
|
// General section
|
||||||
GeneralSection(
|
GeneralSection(
|
||||||
|
|
@ -118,12 +116,12 @@ fun PreferencesRootView(
|
||||||
onSignOutClick = onSignOutClick,
|
onSignOutClick = onSignOutClick,
|
||||||
onDeactivateClick = onDeactivateClick,
|
onDeactivateClick = onDeactivateClick,
|
||||||
)
|
)
|
||||||
|
// Version
|
||||||
Footer(
|
Footer(
|
||||||
version = state.version,
|
version = state.version,
|
||||||
deviceId = state.deviceId,
|
deviceId = state.deviceId,
|
||||||
onClick = if (!state.showDeveloperSettings) {
|
onClick = if (!state.showDeveloperSettings) {
|
||||||
{ state.eventSink(PreferencesRootEvents.OnVersionInfoClick) }
|
{ state.eventSink(PreferencesRootEvent.OnVersionInfoClick) }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -142,13 +140,15 @@ private fun ColumnScope.MultiAccountSection(
|
||||||
)
|
)
|
||||||
state.otherSessions.forEach { matrixUser ->
|
state.otherSessions.forEach { matrixUser ->
|
||||||
MatrixUserRow(
|
MatrixUserRow(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier
|
||||||
state.eventSink(PreferencesRootEvents.SwitchToSession(matrixUser.userId))
|
.clickable {
|
||||||
},
|
state.eventSink(PreferencesRootEvent.SwitchToSession(matrixUser.userId))
|
||||||
|
}
|
||||||
|
.padding(top = 2.dp, bottom = 2.dp, end = 8.dp),
|
||||||
matrixUser = matrixUser,
|
matrixUser = matrixUser,
|
||||||
avatarSize = AvatarSize.AccountItem,
|
avatarSize = AvatarSize.AccountItem,
|
||||||
|
verticalSpaceWidth = 16.dp,
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
|
||||||
}
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())),
|
||||||
|
|
@ -198,6 +198,14 @@ private fun ColumnScope.ManageAccountSection(
|
||||||
onLinkNewDeviceClick: () -> Unit,
|
onLinkNewDeviceClick: () -> Unit,
|
||||||
onOpenBlockedUsers: () -> Unit,
|
onOpenBlockedUsers: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
state.accountManagementUrl?.let { url ->
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(id = CommonStrings.action_manage_account_and_devices)) },
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
|
||||||
|
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())),
|
||||||
|
onClick = { onManageAccountClick(url) },
|
||||||
|
)
|
||||||
|
}
|
||||||
if (state.showLinkNewDevice) {
|
if (state.showLinkNewDevice) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.common_link_new_device)) },
|
headlineContent = { Text(stringResource(id = CommonStrings.common_link_new_device)) },
|
||||||
|
|
@ -205,33 +213,15 @@ private fun ColumnScope.ManageAccountSection(
|
||||||
onClick = onLinkNewDeviceClick,
|
onClick = onLinkNewDeviceClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
state.accountManagementUrl?.let { url ->
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.action_manage_account)) },
|
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
|
|
||||||
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())),
|
|
||||||
onClick = { onManageAccountClick(url) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.devicesManagementUrl?.let { url ->
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.action_manage_devices)) },
|
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Devices())),
|
|
||||||
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())),
|
|
||||||
onClick = { onManageAccountClick(url) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.showBlockedUsersItem) {
|
if (state.showBlockedUsersItem) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) },
|
headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||||
onClick = onOpenBlockedUsers,
|
onClick = onOpenBlockedUsers,
|
||||||
|
trailingContent = ListItemContent.Text(state.nbOfBlockedUsers.toString()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (state.accountManagementUrl != null || state.showLinkNewDevice || state.showBlockedUsersItem) {
|
||||||
if (state.accountManagementUrl != null || state.devicesManagementUrl != null || state.showBlockedUsersItem) {
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -248,6 +238,18 @@ private fun ColumnScope.GeneralSection(
|
||||||
onSignOutClick: () -> Unit,
|
onSignOutClick: () -> Unit,
|
||||||
onDeactivateClick: () -> Unit,
|
onDeactivateClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(id = CommonStrings.common_advanced_settings)) },
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Settings())),
|
||||||
|
onClick = onOpenAdvancedSettings,
|
||||||
|
)
|
||||||
|
if (state.showLabsItem) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(id = R.string.screen_labs_title)) },
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Labs())),
|
||||||
|
onClick = onOpenLabs,
|
||||||
|
)
|
||||||
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.common_about)) },
|
headlineContent = { Text(stringResource(id = CommonStrings.common_about)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
|
||||||
|
|
@ -267,30 +269,17 @@ private fun ColumnScope.GeneralSection(
|
||||||
onClick = onOpenAnalytics,
|
onClick = onOpenAnalytics,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ListItem(
|
HorizontalDivider()
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.common_advanced_settings)) },
|
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Settings())),
|
|
||||||
onClick = onOpenAdvancedSettings,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (state.showLabsItem) {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(stringResource(id = R.string.screen_labs_title)) },
|
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Labs())),
|
|
||||||
onClick = onOpenLabs,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) },
|
headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.SignOut())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())),
|
||||||
style = ListItemStyle.Destructive,
|
style = ListItemStyle.Destructive,
|
||||||
onClick = onSignOutClick,
|
onClick = onSignOutClick,
|
||||||
)
|
)
|
||||||
if (state.canDeactivateAccount) {
|
if (state.canDeactivateAccount) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.action_deactivate_account)) },
|
headlineContent = { Text(stringResource(id = CommonStrings.action_deactivate_account)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Warning())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())),
|
||||||
style = ListItemStyle.Destructive,
|
style = ListItemStyle.Destructive,
|
||||||
onClick = onDeactivateClick,
|
onClick = onDeactivateClick,
|
||||||
)
|
)
|
||||||
|
|
@ -319,9 +308,8 @@ private fun ColumnScope.Footer(
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
.padding(top = 16.dp)
|
|
||||||
.clickable(enabled = onClick != null, onClick = onClick ?: {})
|
.clickable(enabled = onClick != null, onClick = onClick ?: {})
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 24.dp),
|
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 24.dp),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
text = text,
|
text = text,
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
|
@ -340,19 +328,23 @@ private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
|
||||||
|
|
||||||
@PreviewWithLargeHeight
|
@PreviewWithLargeHeight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
|
internal fun PreferencesRootViewLightPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) =
|
||||||
ElementPreviewLight { ContentToPreview(matrixUser) }
|
ElementPreviewLight(
|
||||||
|
drawableFallbackForImages = CommonDrawables.sample_avatar,
|
||||||
|
) { ContentToPreview(state) }
|
||||||
|
|
||||||
@PreviewWithLargeHeight
|
@PreviewWithLargeHeight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
|
internal fun PreferencesRootViewDarkPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) =
|
||||||
ElementPreviewDark { ContentToPreview(matrixUser) }
|
ElementPreviewDark(
|
||||||
|
drawableFallbackForImages = CommonDrawables.sample_avatar,
|
||||||
|
) { ContentToPreview(state) }
|
||||||
|
|
||||||
@ExcludeFromCoverage
|
@ExcludeFromCoverage
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(matrixUser: MatrixUser) {
|
private fun ContentToPreview(state: PreferencesRootState) {
|
||||||
PreferencesRootView(
|
PreferencesRootView(
|
||||||
state = aPreferencesRootState(myUser = matrixUser),
|
state = state,
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onAddAccountClick = {},
|
onAddAccountClick = {},
|
||||||
onOpenAnalytics = {},
|
onOpenAnalytics = {},
|
||||||
|
|
@ -372,16 +364,3 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||||
onDeactivateClick = {},
|
onDeactivateClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun MultiAccountSectionPreview() = ElementPreview {
|
|
||||||
Column {
|
|
||||||
MultiAccountSection(
|
|
||||||
state = aPreferencesRootState(
|
|
||||||
otherSessions = aMatrixUserList(),
|
|
||||||
),
|
|
||||||
onAddAccountClick = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,21 +15,21 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserHeader
|
import io.element.android.libraries.matrix.ui.components.MatrixUserHeader
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserWithNullProvider
|
import io.element.android.libraries.matrix.ui.components.MatrixUserProvider
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserPreferences(
|
fun UserPreferences(
|
||||||
user: MatrixUser?,
|
matrixUser: MatrixUser,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
MatrixUserHeader(
|
MatrixUserHeader(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
matrixUser = user
|
matrixUser = matrixUser,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserWithNullProvider::class) matrixUser: MatrixUser?) = ElementPreview {
|
internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview {
|
||||||
UserPreferences(matrixUser)
|
UserPreferences(matrixUser)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ 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_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
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_SESSION_ID_2
|
||||||
|
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_NAME
|
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.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
|
|
@ -40,7 +42,9 @@ import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -73,6 +77,7 @@ class PreferencesRootPresenterTest {
|
||||||
assertThat(initialState.version).isEqualTo("A Version")
|
assertThat(initialState.version).isEqualTo("A Version")
|
||||||
assertThat(initialState.isMultiAccountEnabled).isFalse()
|
assertThat(initialState.isMultiAccountEnabled).isFalse()
|
||||||
assertThat(initialState.otherSessions).isEmpty()
|
assertThat(initialState.otherSessions).isEmpty()
|
||||||
|
assertThat(initialState.version).isEqualTo("A Version")
|
||||||
val loadedState = awaitItem()
|
val loadedState = awaitItem()
|
||||||
assertThat(loadedState.myUser).isEqualTo(
|
assertThat(loadedState.myUser).isEqualTo(
|
||||||
MatrixUser(
|
MatrixUser(
|
||||||
|
|
@ -81,27 +86,21 @@ class PreferencesRootPresenterTest {
|
||||||
avatarUrl = AN_AVATAR_URL
|
avatarUrl = AN_AVATAR_URL
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(initialState.version).isEqualTo("A Version")
|
|
||||||
assertThat(loadedState.showSecureBackup).isFalse()
|
assertThat(loadedState.showSecureBackup).isFalse()
|
||||||
assertThat(loadedState.showSecureBackupBadge).isFalse()
|
assertThat(loadedState.showSecureBackupBadge).isFalse()
|
||||||
assertThat(loadedState.accountManagementUrl).isNull()
|
assertThat(loadedState.accountManagementUrl).isNull()
|
||||||
assertThat(loadedState.devicesManagementUrl).isNull()
|
|
||||||
assertThat(loadedState.showAnalyticsSettings).isFalse()
|
assertThat(loadedState.showAnalyticsSettings).isFalse()
|
||||||
assertThat(loadedState.showLinkNewDevice).isFalse()
|
assertThat(loadedState.showLinkNewDevice).isFalse()
|
||||||
assertThat(loadedState.showDeveloperSettings).isTrue()
|
assertThat(loadedState.showDeveloperSettings).isTrue()
|
||||||
assertThat(loadedState.canDeactivateAccount).isTrue()
|
assertThat(loadedState.canDeactivateAccount).isTrue()
|
||||||
assertThat(loadedState.canReportBug).isTrue()
|
assertThat(loadedState.canReportBug).isTrue()
|
||||||
|
assertThat(loadedState.nbOfBlockedUsers).isEqualTo(0)
|
||||||
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
|
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
|
||||||
assertThat(loadedState.snackbarMessage).isNull()
|
assertThat(loadedState.snackbarMessage).isNull()
|
||||||
skipItems(1)
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
accountManagementUrlResult.assertions().isCalledExactly(2)
|
accountManagementUrlResult.assertions().isCalledOnce()
|
||||||
.withSequence(
|
.with(value(null))
|
||||||
listOf(value(AccountManagementAction.Profile)),
|
assertThat(finalState.accountManagementUrl).isEqualTo("null url")
|
||||||
listOf(value(AccountManagementAction.DevicesList)),
|
|
||||||
)
|
|
||||||
assertThat(finalState.accountManagementUrl).isEqualTo("Profile url")
|
|
||||||
assertThat(finalState.devicesManagementUrl).isEqualTo("DevicesList url")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,6 +120,22 @@ class PreferencesRootPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - number of blocked users`() = runTest {
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
canDeactivateAccountResult = { true },
|
||||||
|
accountManagementUrlResult = { Result.success("") },
|
||||||
|
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2)),
|
||||||
|
)
|
||||||
|
createPresenter(
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
).test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.nbOfBlockedUsers).isEqualTo(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - secure backup badge`() = runTest {
|
fun `present - secure backup badge`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
|
|
@ -181,12 +196,36 @@ class PreferencesRootPresenterTest {
|
||||||
val loadedState = awaitFirstItem()
|
val loadedState = awaitFirstItem()
|
||||||
repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) {
|
repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) {
|
||||||
assertThat(loadedState.showDeveloperSettings).isFalse()
|
assertThat(loadedState.showDeveloperSettings).isFalse()
|
||||||
loadedState.eventSink(PreferencesRootEvents.OnVersionInfoClick)
|
loadedState.eventSink(PreferencesRootEvent.OnVersionInfoClick)
|
||||||
}
|
}
|
||||||
assertThat(awaitItem().showDeveloperSettings).isTrue()
|
assertThat(awaitItem().showDeveloperSettings).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - switch session invoke method on the session store`() = runTest {
|
||||||
|
val setLatestSessionResult = lambdaRecorder<String, Unit> { }
|
||||||
|
val sessionStore = InMemorySessionStore(
|
||||||
|
initialList = listOf(
|
||||||
|
aSessionData(sessionId = A_SESSION_ID.value),
|
||||||
|
aSessionData(sessionId = A_SESSION_ID_2.value),
|
||||||
|
),
|
||||||
|
setLatestSessionResult = setLatestSessionResult,
|
||||||
|
)
|
||||||
|
createPresenter(
|
||||||
|
matrixClient = FakeMatrixClient(
|
||||||
|
canDeactivateAccountResult = { true },
|
||||||
|
accountManagementUrlResult = { Result.success(null) },
|
||||||
|
),
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
).test {
|
||||||
|
val loadedState = awaitFirstItem()
|
||||||
|
loadedState.eventSink(PreferencesRootEvent.SwitchToSession(A_SESSION_ID_2))
|
||||||
|
setLatestSessionResult.assertions().isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID_2.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest {
|
fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest {
|
||||||
createPresenter(
|
createPresenter(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,483 @@
|
||||||
|
/*
|
||||||
|
* 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.preferences.impl.root
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.preferences.impl.R
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class PreferencesRootViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invokes back callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackClick = callback,
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on User profile invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
val user = aMatrixUser()
|
||||||
|
ensureCalledOnceWithParam(user) { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
myUser = user,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenUserProfile = callback,
|
||||||
|
)
|
||||||
|
rule.onNodeWithText("Alice").performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on other session sends a SwitchToSession`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>()
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
isMultiAccountEnabled = true,
|
||||||
|
otherSessions = listOf(
|
||||||
|
aMatrixUser(
|
||||||
|
id = A_USER_ID_2.value,
|
||||||
|
displayName = "Bob",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText("Bob").performClick()
|
||||||
|
eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Add account invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
isMultiAccountEnabled = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onAddAccountClick = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_add_another_account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when multi account is not enabled, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
isMultiAccountEnabled = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_add_another_account)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Encryption invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showSecureBackup = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onSecureBackupClick = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_encryption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when showSecureBackup is false, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showSecureBackup = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_encryption)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Manage account invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnceWithParam("aUrl") { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
accountManagementUrl = "aUrl",
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onManageAccountClick = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_manage_account_and_devices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when accountManagementUrl is null, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
accountManagementUrl = null,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Link new devices invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showLinkNewDevice = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onLinkNewDeviceClick = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_link_new_device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when showLinkNewDevice is false, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showLinkNewDevice = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_link_new_device)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Analytics invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showAnalyticsSettings = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenAnalytics = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_analytics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when showAnalyticsSettings is false, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showAnalyticsSettings = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_analytics)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Report a problem invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
canReportBug = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenRageShake = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_report_a_problem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when canReportBug is false, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
canReportBug = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Screen lock invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenLockScreenSettings = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_screen_lock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on About invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenAbout = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_about)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Developer settings invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showDeveloperSettings = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenDeveloperSettings = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_developer_options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when showDeveloperSettings is false, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showDeveloperSettings = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_developer_options)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Advanced settings invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenAdvancedSettings = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_advanced_settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Labs invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showLabsItem = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenLabs = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_labs_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when showLabsItem is false, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
showLabsItem = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(R.string.screen_labs_title)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Notification invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenNotificationSettings = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_notification_settings_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Blocked users invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
nbOfBlockedUsers = 1,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenBlockedUsers = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.common_blocked_users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when nbOfBlockedUsers is 0, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
nbOfBlockedUsers = 0,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_blocked_users)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Remove this device invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onSignOutClick = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_signout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on Deactivate invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
canDeactivateAccount = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onDeactivateClick = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_deactivate_account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when canDeactivateAccount is false, item is not shown`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
canDeactivateAccount = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_deactivate_account)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on version sends a PreferencesRootEvents`() {
|
||||||
|
val version = "VERSION"
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>()
|
||||||
|
rule.setView(
|
||||||
|
aPreferencesRootState(
|
||||||
|
version = version,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(version).performClick()
|
||||||
|
eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView(
|
||||||
|
state: PreferencesRootState,
|
||||||
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onAddAccountClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onSecureBackupClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenAnalytics: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenRageShake: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenLockScreenSettings: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenAbout: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenDeveloperSettings: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenAdvancedSettings: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenLabs: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenNotificationSettings: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenUserProfile: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onOpenBlockedUsers: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onSignOutClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onDeactivateClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
PreferencesRootView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onAddAccountClick = onAddAccountClick,
|
||||||
|
onSecureBackupClick = onSecureBackupClick,
|
||||||
|
onManageAccountClick = onManageAccountClick,
|
||||||
|
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||||
|
onOpenAnalytics = onOpenAnalytics,
|
||||||
|
onOpenRageShake = onOpenRageShake,
|
||||||
|
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||||
|
onOpenAbout = onOpenAbout,
|
||||||
|
onOpenDeveloperSettings = onOpenDeveloperSettings,
|
||||||
|
onOpenAdvancedSettings = onOpenAdvancedSettings,
|
||||||
|
onOpenLabs = onOpenLabs,
|
||||||
|
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||||
|
onOpenUserProfile = onOpenUserProfile,
|
||||||
|
onOpenBlockedUsers = onOpenBlockedUsers,
|
||||||
|
onSignOutClick = onSignOutClick,
|
||||||
|
onDeactivateClick = onDeactivateClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ enum class AvatarSize(val dp: Dp) {
|
||||||
|
|
||||||
RoomSelectRoomListItem(36.dp),
|
RoomSelectRoomListItem(36.dp),
|
||||||
|
|
||||||
UserPreference(56.dp),
|
UserPreference(52.dp),
|
||||||
|
|
||||||
UserHeader(96.dp),
|
UserHeader(96.dp),
|
||||||
UserListItem(36.dp),
|
UserListItem(36.dp),
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,21 @@
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.preview
|
package io.element.android.libraries.designsystem.preview
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ElementPreviewDark(
|
fun ElementPreviewDark(
|
||||||
showBackground: Boolean = true,
|
showBackground: Boolean = true,
|
||||||
content: @Composable () -> Unit
|
@DrawableRes
|
||||||
|
drawableFallbackForImages: Int = CommonDrawables.sample_background,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
ElementPreview(
|
ElementPreview(
|
||||||
darkTheme = true,
|
darkTheme = true,
|
||||||
showBackground = showBackground,
|
showBackground = showBackground,
|
||||||
content = content
|
drawableFallbackForImages = drawableFallbackForImages,
|
||||||
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,21 @@
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.preview
|
package io.element.android.libraries.designsystem.preview
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ElementPreviewLight(
|
fun ElementPreviewLight(
|
||||||
showBackground: Boolean = true,
|
showBackground: Boolean = true,
|
||||||
content: @Composable () -> Unit
|
@DrawableRes
|
||||||
|
drawableFallbackForImages: Int = CommonDrawables.sample_background,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
ElementPreview(
|
ElementPreview(
|
||||||
darkTheme = false,
|
darkTheme = false,
|
||||||
showBackground = showBackground,
|
showBackground = showBackground,
|
||||||
content = content
|
drawableFallbackForImages = drawableFallbackForImages,
|
||||||
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.ui.components
|
package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -34,51 +35,34 @@ import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MatrixUserHeader(
|
fun MatrixUserHeader(
|
||||||
matrixUser: MatrixUser?,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
// TODO handle click on this item, to let the user be able to update their profile.
|
|
||||||
// onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
if (matrixUser == null) {
|
|
||||||
MatrixUserHeaderPlaceholder(modifier = modifier)
|
|
||||||
} else {
|
|
||||||
MatrixUserHeaderContent(
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
modifier = modifier,
|
|
||||||
// onClick = onClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MatrixUserHeaderContent(
|
|
||||||
matrixUser: MatrixUser,
|
matrixUser: MatrixUser,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
// onClick: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
// .clickable(onClick = onClick)
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 12.dp),
|
.padding(vertical = 7.dp),
|
||||||
avatarData = matrixUser.getAvatarData(size = AvatarSize.UserPreference),
|
avatarData = matrixUser.getAvatarData(size = AvatarSize.UserPreference),
|
||||||
avatarType = AvatarType.User,
|
avatarType = AvatarType.User,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(13.dp))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
// Name
|
// Name
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.clipToBounds(),
|
modifier = Modifier.clipToBounds(),
|
||||||
text = matrixUser.getBestName(),
|
text = matrixUser.getBestName(),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
style = ElementTheme.typography.fontHeadingMdRegular,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = ElementTheme.colors.textPrimary,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2023-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.libraries.matrix.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.placeholderBackground
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MatrixUserHeaderPlaceholder(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 12.dp)
|
|
||||||
.size(AvatarSize.UserPreference.dp)
|
|
||||||
.background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
PlaceholderAtom(width = 80.dp, height = 7.dp)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
PlaceholderAtom(width = 180.dp, height = 6.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun MatrixUserHeaderPlaceholderPreview() = ElementPreview {
|
|
||||||
MatrixUserHeaderPlaceholder()
|
|
||||||
}
|
|
||||||
|
|
@ -20,15 +20,6 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
open class MatrixUserWithNullProvider : PreviewParameterProvider<MatrixUser?> {
|
|
||||||
override val values: Sequence<MatrixUser?>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aMatrixUser(),
|
|
||||||
aMatrixUser(displayName = null),
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
open class MatrixUserWithAvatarProvider : PreviewParameterProvider<MatrixUser?> {
|
open class MatrixUserWithAvatarProvider : PreviewParameterProvider<MatrixUser?> {
|
||||||
override val values: Sequence<MatrixUser?>
|
override val values: Sequence<MatrixUser?>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ package io.element.android.libraries.matrix.ui.components
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -23,12 +25,14 @@ fun MatrixUserRow(
|
||||||
matrixUser: MatrixUser,
|
matrixUser: MatrixUser,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
avatarSize: AvatarSize = AvatarSize.UserListItem,
|
avatarSize: AvatarSize = AvatarSize.UserListItem,
|
||||||
|
verticalSpaceWidth: Dp = 12.dp,
|
||||||
trailingContent: @Composable (() -> Unit)? = null,
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
) = UserRow(
|
) = UserRow(
|
||||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||||
name = matrixUser.getBestName(),
|
name = matrixUser.getBestName(),
|
||||||
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
verticalSpaceWidth = verticalSpaceWidth,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,16 @@ package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -31,22 +34,22 @@ internal fun UserRow(
|
||||||
subtext: String?,
|
subtext: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
verticalSpaceWidth: Dp = 12.dp,
|
||||||
trailingContent: @Composable (() -> Unit)? = null,
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
avatarType = AvatarType.User,
|
avatarType = AvatarType.User,
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(verticalSpaceWidth))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.weight(1f),
|
||||||
.padding(start = 12.dp)
|
|
||||||
.weight(1f),
|
|
||||||
) {
|
) {
|
||||||
// Name
|
// Name
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:df19ce5a967143e2cb6d1fe021663f72e36f20c32e912894a2fbad628f03c3e5
|
|
||||||
size 53561
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:8fd24e865907b5c9240829710a910e445954bef9b8575f5115a52837e00d817f
|
|
||||||
size 54591
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:369c835c46e19d3e3171add57055624cf672a9d34109e6c831e0c1bce234c605
|
oid sha256:712c1fca10ed7655634d300c03615c6c4dd2f71b74c178398d72fa0427f0d766
|
||||||
size 39513
|
size 41537
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3d9f6763de5b844eeace37bedb25b125976625394d69d7843eedb26319e926aa
|
oid sha256:da47d339d9b8712aa13c394482f8aa5d2e1fb4fcb8eb10df473394bfec1ef507
|
||||||
size 39316
|
size 25980
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:324ce7d935816d87fea7b70bc7ebaacb0ac1d007b08dba85f03c03a1f045e450
|
||||||
|
size 36764
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9176ee2b5b78032639d9f51f7680d82f7d3ca916fb587d914f12d075382d65f0
|
||||||
|
size 27077
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:821a13ed4effd98ad459e3697a33d9d42500d7f1f46115a97c9b7444303a3bb2
|
||||||
|
size 27645
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e457b722fe403205f1394e7347dedb3aba308e48c979ea002399425c9a130fd2
|
||||||
|
size 20667
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dce8486726293027aedcdd2e67d10a39a1a2c439ca67d81ae247b60119675ada
|
oid sha256:2c149288a8ef258f65292f673b9a15ea34910db6d3bfe2402b2a3264227f2b0d
|
||||||
size 40385
|
size 42547
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:93ee581f59c79e03b9c9311765da4c828c5009d14e92f7cca9bbcee418fdfc63
|
oid sha256:7ecf19446d8a0cf57431f13cbac9331ff72c93637c1cd1b442ed6330566debb2
|
||||||
size 40442
|
size 26879
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:00c7c42f1e6dde16916ae12a889b728c0fb321aa2c3fc6af80ec01d99a3af7a6
|
||||||
|
size 37164
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:74a4913734d0648115d5056052fa2de8a839bb5a4d2dfdaa3d8ed5f0eef2793d
|
||||||
|
size 27728
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e6c20d715bfa287ca0fa78bab1166ff0370b5124455c87f1956e7dc3cb9b3d36
|
||||||
|
size 28253
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3d5b500d6275bc6ead5e8644b1afb1a0a829e91d4912444e5e0d431322343855
|
||||||
|
size 20700
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5ccaa7f88a9e46bdc526bfe3d5c2163bf3d963c0661179db97f62559edfd3189
|
oid sha256:0805bac4bf9e1c5bb16b4f81b004bcc952563eef98101d8c9c6e856a414977d0
|
||||||
size 11042
|
size 11219
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f37e1587ba12f9b6326b5b7398982fc663ca913da8c0ee83dfbd5e9decbd4362
|
oid sha256:b61c5a72e8e63a1775d20717c78d8e68e46c7c22b8e4ab8c24154dc3d48d7d0e
|
||||||
size 10906
|
size 11428
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:b494ecdb4962772dd548339a4ac57be40b273b513697ed6d039d1c905617d54f
|
|
||||||
size 4987
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bcb0063babe7091368af6b5bc7e8929c54ea879bd78043d9128db2dcea9d79fa
|
oid sha256:070163694fe10b465f2903ce2cc88f9ffc67d9489aa5a4e204cd087fd02b8642
|
||||||
size 11191
|
size 11281
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f34e63a88464ddc817d1ffe0324352199ae1821dfa846cceac129a656ece2eb6
|
oid sha256:da4e1512cd7a58ede774755b6e3ac427e90c437bbbadaea8479506af776999c3
|
||||||
size 10911
|
size 11323
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f0618a9f769e15b4e682d763224cc1fe0abf62c58f3b9a6b4059153f8805671e
|
|
||||||
size 4740
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:b494ecdb4962772dd548339a4ac57be40b273b513697ed6d039d1c905617d54f
|
|
||||||
size 4987
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f0618a9f769e15b4e682d763224cc1fe0abf62c58f3b9a6b4059153f8805671e
|
|
||||||
size 4740
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5ccaa7f88a9e46bdc526bfe3d5c2163bf3d963c0661179db97f62559edfd3189
|
oid sha256:0805bac4bf9e1c5bb16b4f81b004bcc952563eef98101d8c9c6e856a414977d0
|
||||||
size 11042
|
size 11219
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f37e1587ba12f9b6326b5b7398982fc663ca913da8c0ee83dfbd5e9decbd4362
|
oid sha256:b61c5a72e8e63a1775d20717c78d8e68e46c7c22b8e4ab8c24154dc3d48d7d0e
|
||||||
size 10906
|
size 11428
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bcb0063babe7091368af6b5bc7e8929c54ea879bd78043d9128db2dcea9d79fa
|
oid sha256:070163694fe10b465f2903ce2cc88f9ffc67d9489aa5a4e204cd087fd02b8642
|
||||||
size 11191
|
size 11281
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f34e63a88464ddc817d1ffe0324352199ae1821dfa846cceac129a656ece2eb6
|
oid sha256:da4e1512cd7a58ede774755b6e3ac427e90c437bbbadaea8479506af776999c3
|
||||||
size 10911
|
size 11323
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue