Merge pull request #6602 from element-hq/feature/bma/updateSettingsUI
Settings UI update.
This commit is contained in:
commit
47a430978f
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.encryption.EncryptionService
|
||||
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.sync.SlidingSyncVersion
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
|
|
@ -177,7 +176,6 @@ class LoggedInPresenter(
|
|||
}
|
||||
|
||||
private fun CoroutineScope.preloadAccountManagementUrl() = launch {
|
||||
matrixClient.getAccountManagementUrl(AccountManagementAction.Profile)
|
||||
matrixClient.getAccountManagementUrl(AccountManagementAction.DevicesList)
|
||||
matrixClient.getAccountManagementUrl(null)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class LoggedInPresenterTest {
|
|||
}
|
||||
|
||||
@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 matrixClient = FakeMatrixClient(
|
||||
accountManagementUrlResult = accountManagementUrlResult,
|
||||
|
|
@ -81,11 +81,8 @@ class LoggedInPresenterTest {
|
|||
).test {
|
||||
awaitItem()
|
||||
advanceUntilIdle()
|
||||
accountManagementUrlResult.assertions().isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value(AccountManagementAction.Profile)),
|
||||
listOf(value(AccountManagementAction.DevicesList)),
|
||||
)
|
||||
accountManagementUrlResult.assertions().isCalledOnce()
|
||||
.with(value(null))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.features.preferences.impl.root
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
sealed interface PreferencesRootEvents {
|
||||
data object OnVersionInfoClick : PreferencesRootEvents
|
||||
data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvents
|
||||
sealed interface PreferencesRootEvent {
|
||||
data object OnVersionInfoClick : PreferencesRootEvent
|
||||
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.matrix.api.MatrixClient
|
||||
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.verification.SessionVerificationService
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
|
|
@ -99,9 +98,6 @@ class PreferencesRootPresenter(
|
|||
val accountManagementUrl: MutableState<String?> = remember {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val devicesManagementUrl: MutableState<String?> = remember {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
var canDeactivateAccount by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
|
@ -110,9 +106,9 @@ class PreferencesRootPresenter(
|
|||
canDeactivateAccount = matrixClient.canDeactivateAccount()
|
||||
}
|
||||
|
||||
val showBlockedUsersItem by produceState(initialValue = false) {
|
||||
val nbOfBlockedUsers by produceState(initialValue = 0) {
|
||||
matrixClient.ignoredUsersFlow
|
||||
.onEach { value = it.isNotEmpty() }
|
||||
.onEach { value = it.size }
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
|
|
@ -121,17 +117,17 @@ class PreferencesRootPresenter(
|
|||
val directLogoutState = directLogoutPresenter.present()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
|
||||
initAccountManagementUrl(accountManagementUrl)
|
||||
}
|
||||
|
||||
val showDeveloperSettings by showDeveloperSettingsProvider.showDeveloperSettings.collectAsState()
|
||||
|
||||
fun handleEvent(event: PreferencesRootEvents) {
|
||||
fun handleEvent(event: PreferencesRootEvent) {
|
||||
when (event) {
|
||||
is PreferencesRootEvents.OnVersionInfoClick -> {
|
||||
is PreferencesRootEvent.OnVersionInfoClick -> {
|
||||
showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope)
|
||||
}
|
||||
is PreferencesRootEvents.SwitchToSession -> coroutineScope.launch {
|
||||
is PreferencesRootEvent.SwitchToSession -> coroutineScope.launch {
|
||||
sessionStore.setLatestSession(event.sessionId.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -146,13 +142,12 @@ class PreferencesRootPresenter(
|
|||
showSecureBackup = !canVerifyUserSession,
|
||||
showSecureBackupBadge = showSecureBackupIndicator,
|
||||
accountManagementUrl = accountManagementUrl.value,
|
||||
devicesManagementUrl = devicesManagementUrl.value,
|
||||
showAnalyticsSettings = hasAnalyticsProviders,
|
||||
canReportBug = canReportBug,
|
||||
showLinkNewDevice = showLinkNewDevice,
|
||||
showDeveloperSettings = showDeveloperSettings,
|
||||
canDeactivateAccount = canDeactivateAccount,
|
||||
showBlockedUsersItem = showBlockedUsersItem,
|
||||
nbOfBlockedUsers = nbOfBlockedUsers,
|
||||
showLabsItem = showLabsItem,
|
||||
directLogoutState = directLogoutState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
|
|
@ -162,9 +157,7 @@ class PreferencesRootPresenter(
|
|||
|
||||
private fun CoroutineScope.initAccountManagementUrl(
|
||||
accountManagementUrl: MutableState<String?>,
|
||||
devicesManagementUrl: MutableState<String?>,
|
||||
) = launch {
|
||||
accountManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.Profile).getOrNull()
|
||||
devicesManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.DevicesList).getOrNull()
|
||||
accountManagementUrl.value = matrixClient.getAccountManagementUrl(null).getOrNull()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,16 @@ data class PreferencesRootState(
|
|||
val showSecureBackup: Boolean,
|
||||
val showSecureBackupBadge: Boolean,
|
||||
val accountManagementUrl: String?,
|
||||
val devicesManagementUrl: String?,
|
||||
val canReportBug: Boolean,
|
||||
val showLinkNewDevice: Boolean,
|
||||
val showAnalyticsSettings: Boolean,
|
||||
val showDeveloperSettings: Boolean,
|
||||
val canDeactivateAccount: Boolean,
|
||||
val showBlockedUsersItem: Boolean,
|
||||
val nbOfBlockedUsers: Int,
|
||||
val showLabsItem: Boolean,
|
||||
val directLogoutState: DirectLogoutState,
|
||||
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
|
||||
|
||||
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.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
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.aMatrixUserList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
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(
|
||||
myUser: MatrixUser = aMatrixUser(),
|
||||
version: String = "Version 1.1 (1)",
|
||||
deviceId: DeviceId? = null,
|
||||
isMultiAccountEnabled: Boolean = false,
|
||||
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(
|
||||
myUser = myUser,
|
||||
version = "Version 1.1 (1)",
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
isMultiAccountEnabled = true,
|
||||
version = version,
|
||||
deviceId = deviceId,
|
||||
isMultiAccountEnabled = isMultiAccountEnabled,
|
||||
otherSessions = otherSessions.toImmutableList(),
|
||||
showSecureBackup = true,
|
||||
showSecureBackupBadge = true,
|
||||
accountManagementUrl = "aUrl",
|
||||
devicesManagementUrl = "anOtherUrl",
|
||||
showAnalyticsSettings = true,
|
||||
showLinkNewDevice = true,
|
||||
canReportBug = true,
|
||||
showDeveloperSettings = true,
|
||||
showBlockedUsersItem = true,
|
||||
showLabsItem = true,
|
||||
canDeactivateAccount = true,
|
||||
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
||||
directLogoutState = aDirectLogoutState(),
|
||||
showSecureBackup = showSecureBackup,
|
||||
showSecureBackupBadge = showSecureBackupBadge,
|
||||
accountManagementUrl = accountManagementUrl,
|
||||
canReportBug = canReportBug,
|
||||
showLinkNewDevice = showLinkNewDevice,
|
||||
showAnalyticsSettings = showAnalyticsSettings,
|
||||
showDeveloperSettings = showDeveloperSettings,
|
||||
canDeactivateAccount = canDeactivateAccount,
|
||||
nbOfBlockedUsers = nbOfBlockedUsers,
|
||||
showLabsItem = showLabsItem,
|
||||
directLogoutState = directLogoutState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.list.ListItemContent
|
||||
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.ElementPreviewLight
|
||||
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.IconSource
|
||||
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.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.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
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.aMatrixUserList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
|
|
@ -82,22 +78,17 @@ fun PreferencesRootView(
|
|||
modifier = Modifier.clickable {
|
||||
onOpenUserProfile(state.myUser)
|
||||
},
|
||||
user = state.myUser,
|
||||
matrixUser = state.myUser,
|
||||
)
|
||||
if (state.isMultiAccountEnabled) {
|
||||
MultiAccountSection(
|
||||
state = state,
|
||||
onAddAccountClick = onAddAccountClick,
|
||||
)
|
||||
} else {
|
||||
HorizontalDivider()
|
||||
}
|
||||
// 'Manage my app' section
|
||||
ManageAppSection(
|
||||
state = state,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
)
|
||||
|
||||
// User status will be added here
|
||||
// 'Account' section
|
||||
ManageAccountSection(
|
||||
state = state,
|
||||
|
|
@ -105,6 +96,13 @@ fun PreferencesRootView(
|
|||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
onOpenBlockedUsers = onOpenBlockedUsers
|
||||
)
|
||||
// 'Manage my app' section
|
||||
ManageAppSection(
|
||||
state = state,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
)
|
||||
|
||||
// General section
|
||||
GeneralSection(
|
||||
|
|
@ -118,12 +116,12 @@ fun PreferencesRootView(
|
|||
onSignOutClick = onSignOutClick,
|
||||
onDeactivateClick = onDeactivateClick,
|
||||
)
|
||||
|
||||
// Version
|
||||
Footer(
|
||||
version = state.version,
|
||||
deviceId = state.deviceId,
|
||||
onClick = if (!state.showDeveloperSettings) {
|
||||
{ state.eventSink(PreferencesRootEvents.OnVersionInfoClick) }
|
||||
{ state.eventSink(PreferencesRootEvent.OnVersionInfoClick) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -142,13 +140,15 @@ private fun ColumnScope.MultiAccountSection(
|
|||
)
|
||||
state.otherSessions.forEach { matrixUser ->
|
||||
MatrixUserRow(
|
||||
modifier = Modifier.clickable {
|
||||
state.eventSink(PreferencesRootEvents.SwitchToSession(matrixUser.userId))
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
state.eventSink(PreferencesRootEvent.SwitchToSession(matrixUser.userId))
|
||||
}
|
||||
.padding(top = 2.dp, bottom = 2.dp, end = 8.dp),
|
||||
matrixUser = matrixUser,
|
||||
avatarSize = AvatarSize.AccountItem,
|
||||
verticalSpaceWidth = 16.dp,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
ListItem(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())),
|
||||
|
|
@ -198,6 +198,14 @@ private fun ColumnScope.ManageAccountSection(
|
|||
onLinkNewDeviceClick: () -> 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) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_link_new_device)) },
|
||||
|
|
@ -205,33 +213,15 @@ private fun ColumnScope.ManageAccountSection(
|
|||
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) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||
onClick = onOpenBlockedUsers,
|
||||
trailingContent = ListItemContent.Text(state.nbOfBlockedUsers.toString()),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.accountManagementUrl != null || state.devicesManagementUrl != null || state.showBlockedUsersItem) {
|
||||
if (state.accountManagementUrl != null || state.showLinkNewDevice || state.showBlockedUsersItem) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
|
@ -248,6 +238,18 @@ private fun ColumnScope.GeneralSection(
|
|||
onSignOutClick: () -> 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(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_about)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
|
||||
|
|
@ -267,30 +269,17 @@ private fun ColumnScope.GeneralSection(
|
|||
onClick = onOpenAnalytics,
|
||||
)
|
||||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.SignOut())),
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())),
|
||||
style = ListItemStyle.Destructive,
|
||||
onClick = onSignOutClick,
|
||||
)
|
||||
if (state.canDeactivateAccount) {
|
||||
ListItem(
|
||||
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,
|
||||
onClick = onDeactivateClick,
|
||||
)
|
||||
|
|
@ -319,9 +308,8 @@ private fun ColumnScope.Footer(
|
|||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 16.dp)
|
||||
.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,
|
||||
text = text,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
|
|
@ -340,19 +328,23 @@ private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
|
|||
|
||||
@PreviewWithLargeHeight
|
||||
@Composable
|
||||
internal fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
|
||||
ElementPreviewLight { ContentToPreview(matrixUser) }
|
||||
internal fun PreferencesRootViewLightPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) =
|
||||
ElementPreviewLight(
|
||||
drawableFallbackForImages = CommonDrawables.sample_avatar,
|
||||
) { ContentToPreview(state) }
|
||||
|
||||
@PreviewWithLargeHeight
|
||||
@Composable
|
||||
internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
|
||||
ElementPreviewDark { ContentToPreview(matrixUser) }
|
||||
internal fun PreferencesRootViewDarkPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) =
|
||||
ElementPreviewDark(
|
||||
drawableFallbackForImages = CommonDrawables.sample_avatar,
|
||||
) { ContentToPreview(state) }
|
||||
|
||||
@ExcludeFromCoverage
|
||||
@Composable
|
||||
private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||
private fun ContentToPreview(state: PreferencesRootState) {
|
||||
PreferencesRootView(
|
||||
state = aPreferencesRootState(myUser = matrixUser),
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onAddAccountClick = {},
|
||||
onOpenAnalytics = {},
|
||||
|
|
@ -372,16 +364,3 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
|||
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.matrix.api.user.MatrixUser
|
||||
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
|
||||
fun UserPreferences(
|
||||
user: MatrixUser?,
|
||||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
MatrixUserHeader(
|
||||
modifier = modifier,
|
||||
matrixUser = user
|
||||
matrixUser = matrixUser,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserWithNullProvider::class) matrixUser: MatrixUser?) = ElementPreview {
|
||||
internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview {
|
||||
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.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
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.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.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -73,6 +77,7 @@ class PreferencesRootPresenterTest {
|
|||
assertThat(initialState.version).isEqualTo("A Version")
|
||||
assertThat(initialState.isMultiAccountEnabled).isFalse()
|
||||
assertThat(initialState.otherSessions).isEmpty()
|
||||
assertThat(initialState.version).isEqualTo("A Version")
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.myUser).isEqualTo(
|
||||
MatrixUser(
|
||||
|
|
@ -81,27 +86,21 @@ class PreferencesRootPresenterTest {
|
|||
avatarUrl = AN_AVATAR_URL
|
||||
)
|
||||
)
|
||||
assertThat(initialState.version).isEqualTo("A Version")
|
||||
assertThat(loadedState.showSecureBackup).isFalse()
|
||||
assertThat(loadedState.showSecureBackupBadge).isFalse()
|
||||
assertThat(loadedState.accountManagementUrl).isNull()
|
||||
assertThat(loadedState.devicesManagementUrl).isNull()
|
||||
assertThat(loadedState.showAnalyticsSettings).isFalse()
|
||||
assertThat(loadedState.showLinkNewDevice).isFalse()
|
||||
assertThat(loadedState.showDeveloperSettings).isTrue()
|
||||
assertThat(loadedState.canDeactivateAccount).isTrue()
|
||||
assertThat(loadedState.canReportBug).isTrue()
|
||||
assertThat(loadedState.nbOfBlockedUsers).isEqualTo(0)
|
||||
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
|
||||
assertThat(loadedState.snackbarMessage).isNull()
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
accountManagementUrlResult.assertions().isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value(AccountManagementAction.Profile)),
|
||||
listOf(value(AccountManagementAction.DevicesList)),
|
||||
)
|
||||
assertThat(finalState.accountManagementUrl).isEqualTo("Profile url")
|
||||
assertThat(finalState.devicesManagementUrl).isEqualTo("DevicesList url")
|
||||
accountManagementUrlResult.assertions().isCalledOnce()
|
||||
.with(value(null))
|
||||
assertThat(finalState.accountManagementUrl).isEqualTo("null 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
|
||||
fun `present - secure backup badge`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
|
|
@ -181,12 +196,36 @@ class PreferencesRootPresenterTest {
|
|||
val loadedState = awaitFirstItem()
|
||||
repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) {
|
||||
assertThat(loadedState.showDeveloperSettings).isFalse()
|
||||
loadedState.eventSink(PreferencesRootEvents.OnVersionInfoClick)
|
||||
loadedState.eventSink(PreferencesRootEvent.OnVersionInfoClick)
|
||||
}
|
||||
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
|
||||
fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest {
|
||||
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),
|
||||
|
||||
UserPreference(56.dp),
|
||||
UserPreference(52.dp),
|
||||
|
||||
UserHeader(96.dp),
|
||||
UserListItem(36.dp),
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.preview
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
@Composable
|
||||
fun ElementPreviewDark(
|
||||
showBackground: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
@DrawableRes
|
||||
drawableFallbackForImages: Int = CommonDrawables.sample_background,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
ElementPreview(
|
||||
darkTheme = true,
|
||||
showBackground = showBackground,
|
||||
content = content
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.preview
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
@Composable
|
||||
fun ElementPreviewLight(
|
||||
showBackground: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
@DrawableRes
|
||||
drawableFallbackForImages: Int = CommonDrawables.sample_background,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
ElementPreview(
|
||||
darkTheme = false,
|
||||
showBackground = showBackground,
|
||||
content = content
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -34,51 +35,34 @@ import io.element.android.libraries.matrix.ui.model.getBestName
|
|||
|
||||
@Composable
|
||||
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,
|
||||
modifier: Modifier = Modifier,
|
||||
// onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
// .clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Avatar(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp),
|
||||
.padding(vertical = 7.dp),
|
||||
avatarData = matrixUser.getAvatarData(size = AvatarSize.UserPreference),
|
||||
avatarType = AvatarType.User,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Spacer(modifier = Modifier.width(13.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
text = matrixUser.getBestName(),
|
||||
maxLines = 1,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
style = ElementTheme.typography.fontHeadingMdRegular,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
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?> {
|
||||
override val values: Sequence<MatrixUser?>
|
||||
get() = sequenceOf(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ package io.element.android.libraries.matrix.ui.components
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -23,12 +25,14 @@ fun MatrixUserRow(
|
|||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
avatarSize: AvatarSize = AvatarSize.UserListItem,
|
||||
verticalSpaceWidth: Dp = 12.dp,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
) = UserRow(
|
||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||
name = matrixUser.getBestName(),
|
||||
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||
modifier = modifier,
|
||||
verticalSpaceWidth = verticalSpaceWidth,
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
|
|
@ -31,22 +34,22 @@ internal fun UserRow(
|
|||
subtext: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
verticalSpaceWidth: Dp = 12.dp,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(verticalSpaceWidth))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.weight(1f),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
// Name
|
||||
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
|
||||
oid sha256:369c835c46e19d3e3171add57055624cf672a9d34109e6c831e0c1bce234c605
|
||||
size 39513
|
||||
oid sha256:712c1fca10ed7655634d300c03615c6c4dd2f71b74c178398d72fa0427f0d766
|
||||
size 41537
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3d9f6763de5b844eeace37bedb25b125976625394d69d7843eedb26319e926aa
|
||||
size 39316
|
||||
oid sha256:da47d339d9b8712aa13c394482f8aa5d2e1fb4fcb8eb10df473394bfec1ef507
|
||||
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
|
||||
oid sha256:dce8486726293027aedcdd2e67d10a39a1a2c439ca67d81ae247b60119675ada
|
||||
size 40385
|
||||
oid sha256:2c149288a8ef258f65292f673b9a15ea34910db6d3bfe2402b2a3264227f2b0d
|
||||
size 42547
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:93ee581f59c79e03b9c9311765da4c828c5009d14e92f7cca9bbcee418fdfc63
|
||||
size 40442
|
||||
oid sha256:7ecf19446d8a0cf57431f13cbac9331ff72c93637c1cd1b442ed6330566debb2
|
||||
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
|
||||
oid sha256:5ccaa7f88a9e46bdc526bfe3d5c2163bf3d963c0661179db97f62559edfd3189
|
||||
size 11042
|
||||
oid sha256:0805bac4bf9e1c5bb16b4f81b004bcc952563eef98101d8c9c6e856a414977d0
|
||||
size 11219
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f37e1587ba12f9b6326b5b7398982fc663ca913da8c0ee83dfbd5e9decbd4362
|
||||
size 10906
|
||||
oid sha256:b61c5a72e8e63a1775d20717c78d8e68e46c7c22b8e4ab8c24154dc3d48d7d0e
|
||||
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
|
||||
oid sha256:bcb0063babe7091368af6b5bc7e8929c54ea879bd78043d9128db2dcea9d79fa
|
||||
size 11191
|
||||
oid sha256:070163694fe10b465f2903ce2cc88f9ffc67d9489aa5a4e204cd087fd02b8642
|
||||
size 11281
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f34e63a88464ddc817d1ffe0324352199ae1821dfa846cceac129a656ece2eb6
|
||||
size 10911
|
||||
oid sha256:da4e1512cd7a58ede774755b6e3ac427e90c437bbbadaea8479506af776999c3
|
||||
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
|
||||
oid sha256:5ccaa7f88a9e46bdc526bfe3d5c2163bf3d963c0661179db97f62559edfd3189
|
||||
size 11042
|
||||
oid sha256:0805bac4bf9e1c5bb16b4f81b004bcc952563eef98101d8c9c6e856a414977d0
|
||||
size 11219
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f37e1587ba12f9b6326b5b7398982fc663ca913da8c0ee83dfbd5e9decbd4362
|
||||
size 10906
|
||||
oid sha256:b61c5a72e8e63a1775d20717c78d8e68e46c7c22b8e4ab8c24154dc3d48d7d0e
|
||||
size 11428
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bcb0063babe7091368af6b5bc7e8929c54ea879bd78043d9128db2dcea9d79fa
|
||||
size 11191
|
||||
oid sha256:070163694fe10b465f2903ce2cc88f9ffc67d9489aa5a4e204cd087fd02b8642
|
||||
size 11281
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f34e63a88464ddc817d1ffe0324352199ae1821dfa846cceac129a656ece2eb6
|
||||
size 10911
|
||||
oid sha256:da4e1512cd7a58ede774755b6e3ac427e90c437bbbadaea8479506af776999c3
|
||||
size 11323
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue