mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Merge pull request #7788 from element-hq/feature/bma/removeAccount
Improve the sign out screen
This commit is contained in:
@@ -2,17 +2,25 @@ appId: ${MAESTRO_APP_ID}
|
|||||||
---
|
---
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "home_screen-settings"
|
id: "home_screen-settings"
|
||||||
- tapOn:
|
|
||||||
id: "settings-user_profile"
|
|
||||||
- assertVisible: "Your account"
|
|
||||||
- tapOn: "Remove this device"
|
- tapOn: "Remove this device"
|
||||||
- takeScreenshot: build/maestro/900-SignOutScreen
|
- takeScreenshot: build/maestro/900-SignOutScreen
|
||||||
- back
|
- back
|
||||||
- tapOn: "Remove this device"
|
- tapOn: "Remove this device"
|
||||||
|
# Ensure pressing back cancels
|
||||||
|
- back
|
||||||
|
- tapOn: "Remove this device"
|
||||||
# Ensure cancel cancels
|
# Ensure cancel cancels
|
||||||
|
- tapOn: "Cancel"
|
||||||
|
- tapOn: "Remove this device"
|
||||||
|
- tapOn:
|
||||||
|
text: "Remove this device"
|
||||||
|
index: 1
|
||||||
|
# Cancel the dialog cancels the logout
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "dialog-negative"
|
id: "dialog-negative"
|
||||||
- tapOn: "Remove this device"
|
- tapOn:
|
||||||
|
text: "Remove this device"
|
||||||
|
index: 1
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "dialog-positive"
|
id: "dialog-positive"
|
||||||
- runFlow: ../assertions/assertInitDisplayed.yaml
|
- runFlow: ../assertions/assertInitDisplayed.yaml
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ dependencies {
|
|||||||
implementation(projects.features.announcement.api)
|
implementation(projects.features.announcement.api)
|
||||||
implementation(projects.features.invite.api)
|
implementation(projects.features.invite.api)
|
||||||
implementation(projects.features.networkmonitor.api)
|
implementation(projects.features.networkmonitor.api)
|
||||||
implementation(projects.features.logout.api)
|
|
||||||
implementation(projects.features.leaveroom.api)
|
implementation(projects.features.leaveroom.api)
|
||||||
implementation(projects.features.rageshake.api)
|
implementation(projects.features.rageshake.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
@@ -67,7 +66,6 @@ dependencies {
|
|||||||
testCommonDependencies(libs, true)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(projects.features.announcement.test)
|
testImplementation(projects.features.announcement.test)
|
||||||
testImplementation(projects.features.invite.test)
|
testImplementation(projects.features.invite.test)
|
||||||
testImplementation(projects.features.logout.test)
|
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
|
|||||||
-3
@@ -40,7 +40,6 @@ import io.element.android.features.invite.api.InviteData
|
|||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||||
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
|
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomRenderer
|
import io.element.android.features.leaveroom.api.LeaveRoomRenderer
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
|
||||||
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
|
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
|
||||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
|
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
|
||||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
|
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
|
||||||
@@ -83,7 +82,6 @@ class HomeFlowNode(
|
|||||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||||
private val directLogoutView: DirectLogoutView,
|
|
||||||
private val reportRoomEntryPoint: ReportRoomEntryPoint,
|
private val reportRoomEntryPoint: ReportRoomEntryPoint,
|
||||||
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
|
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
|
||||||
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||||
@@ -248,7 +246,6 @@ class HomeFlowNode(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
directLogoutView.Render(state.directLogoutState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-4
@@ -21,7 +21,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||||
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
@@ -42,7 +41,6 @@ class HomePresenter(
|
|||||||
private val indicatorService: IndicatorService,
|
private val indicatorService: IndicatorService,
|
||||||
private val roomListPresenter: Presenter<RoomListState>,
|
private val roomListPresenter: Presenter<RoomListState>,
|
||||||
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
|
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
|
||||||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
|
||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
private val sessionStore: SessionStore,
|
private val sessionStore: SessionStore,
|
||||||
) : Presenter<HomeState> {
|
) : Presenter<HomeState> {
|
||||||
@@ -75,7 +73,6 @@ class HomePresenter(
|
|||||||
}
|
}
|
||||||
// Avatar indicator
|
// Avatar indicator
|
||||||
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
||||||
val directLogoutState = logoutPresenter.present()
|
|
||||||
|
|
||||||
fun handleEvent(event: HomeEvent) {
|
fun handleEvent(event: HomeEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -98,7 +95,6 @@ class HomePresenter(
|
|||||||
homeSpacesState = homeSpacesState,
|
homeSpacesState = homeSpacesState,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
directLogoutState = directLogoutState,
|
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ package io.element.android.features.home.impl
|
|||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||||
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
|
||||||
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -29,7 +28,6 @@ data class HomeState(
|
|||||||
val homeSpacesState: HomeSpacesState,
|
val homeSpacesState: HomeSpacesState,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
val directLogoutState: DirectLogoutState,
|
|
||||||
val eventSink: (HomeEvent) -> Unit,
|
val eventSink: (HomeEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
|
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
|
||||||
|
|||||||
-4
@@ -16,8 +16,6 @@ import io.element.android.features.home.impl.roomlist.aRoomsContentState
|
|||||||
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
|
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
|
||||||
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
||||||
import io.element.android.features.home.impl.spaces.aHomeSpacesState
|
import io.element.android.features.home.impl.spaces.aHomeSpacesState
|
||||||
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.designsystem.utils.snackbar.SnackbarMessage
|
||||||
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.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
@@ -63,7 +61,6 @@ internal fun aHomeState(
|
|||||||
roomListState: RoomListState = aRoomListState(),
|
roomListState: RoomListState = aRoomListState(),
|
||||||
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
||||||
canReportBug: Boolean = true,
|
canReportBug: Boolean = true,
|
||||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
|
||||||
eventSink: (HomeEvent) -> Unit = {}
|
eventSink: (HomeEvent) -> Unit = {}
|
||||||
) = HomeState(
|
) = HomeState(
|
||||||
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
|
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
|
||||||
@@ -71,7 +68,6 @@ internal fun aHomeState(
|
|||||||
hasNetworkConnection = hasNetworkConnection,
|
hasNetworkConnection = hasNetworkConnection,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
directLogoutState = directLogoutState,
|
|
||||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||||
roomListState = roomListState,
|
roomListState = roomListState,
|
||||||
homeSpacesState = homeSpacesState,
|
homeSpacesState = homeSpacesState,
|
||||||
|
|||||||
-1
@@ -36,7 +36,6 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
|
|||||||
inviteFriendsUseCase = { lambdaError() },
|
inviteFriendsUseCase = { lambdaError() },
|
||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
|
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
|
||||||
directLogoutView = { _ -> lambdaError() },
|
|
||||||
reportRoomEntryPoint = { _, _, _ -> lambdaError() },
|
reportRoomEntryPoint = { _, _, _ -> lambdaError() },
|
||||||
declineInviteAndBlockUserEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
|
declineInviteAndBlockUserEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
|
||||||
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
|
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
|
||||||
|
|||||||
-2
@@ -12,7 +12,6 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.features.home.impl.roomlist.aRoomListState
|
import io.element.android.features.home.impl.roomlist.aRoomListState
|
||||||
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
||||||
import io.element.android.features.home.impl.spaces.aHomeSpacesState
|
import io.element.android.features.home.impl.spaces.aHomeSpacesState
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
@@ -162,7 +161,6 @@ internal fun createHomePresenter(
|
|||||||
indicatorService = indicatorService,
|
indicatorService = indicatorService,
|
||||||
roomListPresenter = { aRoomListState() },
|
roomListPresenter = { aRoomListState() },
|
||||||
homeSpacesPresenter = homeSpacesPresenter,
|
homeSpacesPresenter = homeSpacesPresenter,
|
||||||
logoutPresenter = { aDirectLogoutState() },
|
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
)
|
)
|
||||||
|
|||||||
-1
@@ -11,7 +11,6 @@ package io.element.android.features.logout.api.direct
|
|||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
data class DirectLogoutState(
|
data class DirectLogoutState(
|
||||||
val canDoDirectSignOut: Boolean,
|
|
||||||
val logoutAction: AsyncAction<Unit>,
|
val logoutAction: AsyncAction<Unit>,
|
||||||
val eventSink: (DirectLogoutEvent) -> Unit,
|
val eventSink: (DirectLogoutEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
-2
@@ -23,11 +23,9 @@ open class DirectLogoutStatePreviewParam : PreviewParameterProvider<DirectLogout
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun aDirectLogoutState(
|
fun aDirectLogoutState(
|
||||||
canDoDirectSignOut: Boolean = true,
|
|
||||||
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (DirectLogoutEvent) -> Unit = {},
|
eventSink: (DirectLogoutEvent) -> Unit = {},
|
||||||
) = DirectLogoutState(
|
) = DirectLogoutState(
|
||||||
canDoDirectSignOut = canDoDirectSignOut,
|
|
||||||
logoutAction = logoutAction,
|
logoutAction = logoutAction,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|||||||
+2
@@ -41,6 +41,7 @@ class LogoutPresenter(
|
|||||||
@Composable
|
@Composable
|
||||||
override fun present(): LogoutState {
|
override fun present(): LogoutState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
val currentUser by matrixClient.userProfile.collectAsState()
|
||||||
val logoutAction: MutableState<AsyncAction<Unit>> = remember {
|
val logoutAction: MutableState<AsyncAction<Unit>> = remember {
|
||||||
mutableStateOf(AsyncAction.Uninitialized)
|
mutableStateOf(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
@@ -90,6 +91,7 @@ class LogoutPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return LogoutState(
|
return LogoutState(
|
||||||
|
currentUser = currentUser,
|
||||||
isLastDevice = isLastDevice,
|
isLastDevice = isLastDevice,
|
||||||
backupState = backupState,
|
backupState = backupState,
|
||||||
doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(),
|
doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(),
|
||||||
|
|||||||
+2
@@ -12,8 +12,10 @@ import io.element.android.libraries.architecture.AsyncAction
|
|||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||||
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.user.MatrixUser
|
||||||
|
|
||||||
data class LogoutState(
|
data class LogoutState(
|
||||||
|
val currentUser: MatrixUser,
|
||||||
val isLastDevice: Boolean,
|
val isLastDevice: Boolean,
|
||||||
val backupState: BackupState,
|
val backupState: BackupState,
|
||||||
val doesBackupExistOnServer: Boolean,
|
val doesBackupExistOnServer: Boolean,
|
||||||
|
|||||||
+5
@@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.api.encryption.BackupState
|
|||||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||||
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.encryption.SteadyStateException
|
import io.element.android.libraries.matrix.api.encryption.SteadyStateException
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
|
||||||
open class LogoutStatePreviewParam : PreviewParameterProvider<LogoutState> {
|
open class LogoutStatePreviewParam : PreviewParameterProvider<LogoutState> {
|
||||||
override val values: Sequence<LogoutState>
|
override val values: Sequence<LogoutState>
|
||||||
@@ -39,10 +41,12 @@ open class LogoutStatePreviewParam : PreviewParameterProvider<LogoutState> {
|
|||||||
backupUploadState = BackupUploadState.Waiting,
|
backupUploadState = BackupUploadState.Waiting,
|
||||||
waitingForALongTime = true,
|
waitingForALongTime = true,
|
||||||
),
|
),
|
||||||
|
aLogoutState(currentUser = aMatrixUser(id = "@alice:server.org", displayName = null)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aLogoutState(
|
fun aLogoutState(
|
||||||
|
currentUser: MatrixUser = aMatrixUser(),
|
||||||
isLastDevice: Boolean = false,
|
isLastDevice: Boolean = false,
|
||||||
backupState: BackupState = BackupState.ENABLED,
|
backupState: BackupState = BackupState.ENABLED,
|
||||||
doesBackupExistOnServer: Boolean = true,
|
doesBackupExistOnServer: Boolean = true,
|
||||||
@@ -52,6 +56,7 @@ fun aLogoutState(
|
|||||||
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (LogoutEvent) -> Unit = {},
|
eventSink: (LogoutEvent) -> Unit = {},
|
||||||
) = LogoutState(
|
) = LogoutState(
|
||||||
|
currentUser = currentUser,
|
||||||
isLastDevice = isLastDevice,
|
isLastDevice = isLastDevice,
|
||||||
backupState = backupState,
|
backupState = backupState,
|
||||||
doesBackupExistOnServer = doesBackupExistOnServer,
|
doesBackupExistOnServer = doesBackupExistOnServer,
|
||||||
|
|||||||
+30
-3
@@ -11,7 +11,9 @@ package io.element.android.features.logout.impl
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -32,15 +34,22 @@ import io.element.android.libraries.designsystem.theme.components.Button
|
|||||||
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
|
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||||
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.encryption.SteadyStateException
|
import io.element.android.libraries.matrix.api.encryption.SteadyStateException
|
||||||
|
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCard
|
||||||
|
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCardMode
|
||||||
|
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCardState
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=442-34581
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun LogoutView(
|
fun LogoutView(
|
||||||
state: LogoutState,
|
state: LogoutState,
|
||||||
@@ -54,15 +63,17 @@ fun LogoutView(
|
|||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
title = title(state),
|
title = title(state),
|
||||||
subTitle = subtitle(state),
|
subTitle = subtitle(state),
|
||||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
|
iconStyle = BigIcon.Style.Default(CompoundIcons.Mobile()),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
isScrollable = true,
|
||||||
buttons = {
|
buttons = {
|
||||||
Buttons(
|
Buttons(
|
||||||
state = state,
|
state = state,
|
||||||
onChangeRecoveryKeyClick = onChangeRecoveryKeyClick,
|
onChangeRecoveryKeyClick = onChangeRecoveryKeyClick,
|
||||||
onLogoutClick = {
|
onLogoutClick = {
|
||||||
eventSink(LogoutEvent.Logout(ignoreSdkError = false))
|
eventSink(LogoutEvent.Logout(ignoreSdkError = false))
|
||||||
}
|
},
|
||||||
|
onCancelClick = onBackClick,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -71,6 +82,8 @@ fun LogoutView(
|
|||||||
|
|
||||||
LogoutActionDialog(
|
LogoutActionDialog(
|
||||||
state.logoutAction,
|
state.logoutAction,
|
||||||
|
// Do not show the progress dialog, the progress is rendered in the button.
|
||||||
|
showProgressDialog = false,
|
||||||
onConfirmClick = {
|
onConfirmClick = {
|
||||||
eventSink(LogoutEvent.Logout(ignoreSdkError = false))
|
eventSink(LogoutEvent.Logout(ignoreSdkError = false))
|
||||||
},
|
},
|
||||||
@@ -116,6 +129,7 @@ private fun ColumnScope.Buttons(
|
|||||||
state: LogoutState,
|
state: LogoutState,
|
||||||
onLogoutClick: () -> Unit,
|
onLogoutClick: () -> Unit,
|
||||||
onChangeRecoveryKeyClick: () -> Unit,
|
onChangeRecoveryKeyClick: () -> Unit,
|
||||||
|
onCancelClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val logoutAction = state.logoutAction
|
val logoutAction = state.logoutAction
|
||||||
if (state.isLastDevice) {
|
if (state.isLastDevice) {
|
||||||
@@ -139,6 +153,12 @@ private fun ColumnScope.Buttons(
|
|||||||
.testTag(TestTags.signOut),
|
.testTag(TestTags.signOut),
|
||||||
onClick = onLogoutClick,
|
onClick = onLogoutClick,
|
||||||
)
|
)
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(id = CommonStrings.action_cancel),
|
||||||
|
enabled = logoutAction !is AsyncAction.Loading,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = onCancelClick,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -149,9 +169,16 @@ private fun Content(
|
|||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 60.dp, start = 20.dp, end = 20.dp),
|
.padding(top = 8.dp, start = 16.dp, end = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
AccountInfoCard(
|
||||||
|
AccountInfoCardState(
|
||||||
|
matrixUser = state.currentUser,
|
||||||
|
mode = AccountInfoCardMode.Simple,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
when (state.backupUploadState) {
|
when (state.backupUploadState) {
|
||||||
is BackupUploadState.Uploading -> {
|
is BackupUploadState.Uploading -> {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
|
|||||||
+1
@@ -27,6 +27,7 @@ class DefaultDirectLogoutView : DirectLogoutView {
|
|||||||
val eventSink = state.eventSink
|
val eventSink = state.eventSink
|
||||||
LogoutActionDialog(
|
LogoutActionDialog(
|
||||||
state.logoutAction,
|
state.logoutAction,
|
||||||
|
showProgressDialog = true,
|
||||||
onConfirmClick = {
|
onConfirmClick = {
|
||||||
eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
|
eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
|
||||||
},
|
},
|
||||||
|
|||||||
-15
@@ -10,28 +10,22 @@ package io.element.android.features.logout.impl.direct
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutEvent
|
import io.element.android.features.logout.api.direct.DirectLogoutEvent
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.features.logout.impl.tools.isBackingUp
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class DirectLogoutPresenter(
|
class DirectLogoutPresenter(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val encryptionService: EncryptionService,
|
|
||||||
) : Presenter<DirectLogoutState> {
|
) : Presenter<DirectLogoutState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): DirectLogoutState {
|
override fun present(): DirectLogoutState {
|
||||||
@@ -41,13 +35,6 @@ class DirectLogoutPresenter(
|
|||||||
mutableStateOf(AsyncAction.Uninitialized)
|
mutableStateOf(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
val backupUploadState: BackupUploadState by remember {
|
|
||||||
encryptionService.waitForBackupUploadSteadyState()
|
|
||||||
}
|
|
||||||
.collectAsState(initial = BackupUploadState.Unknown)
|
|
||||||
|
|
||||||
val isLastDevice by encryptionService.isLastDevice.collectAsState()
|
|
||||||
|
|
||||||
fun handleEvent(event: DirectLogoutEvent) {
|
fun handleEvent(event: DirectLogoutEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is DirectLogoutEvent.Logout -> {
|
is DirectLogoutEvent.Logout -> {
|
||||||
@@ -64,8 +51,6 @@ class DirectLogoutPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return DirectLogoutState(
|
return DirectLogoutState(
|
||||||
canDoDirectSignOut = !isLastDevice &&
|
|
||||||
!backupUploadState.isBackingUp(),
|
|
||||||
logoutAction = logoutAction.value,
|
logoutAction = logoutAction.value,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-1
@@ -19,6 +19,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||||||
@Composable
|
@Composable
|
||||||
fun LogoutActionDialog(
|
fun LogoutActionDialog(
|
||||||
state: AsyncAction<Unit>,
|
state: AsyncAction<Unit>,
|
||||||
|
showProgressDialog: Boolean,
|
||||||
onConfirmClick: () -> Unit,
|
onConfirmClick: () -> Unit,
|
||||||
onForceLogoutClick: () -> Unit,
|
onForceLogoutClick: () -> Unit,
|
||||||
onDismissDialog: () -> Unit,
|
onDismissDialog: () -> Unit,
|
||||||
@@ -31,8 +32,11 @@ fun LogoutActionDialog(
|
|||||||
onSubmitClick = onConfirmClick,
|
onSubmitClick = onConfirmClick,
|
||||||
onDismiss = onDismissDialog
|
onDismiss = onDismissDialog
|
||||||
)
|
)
|
||||||
is AsyncAction.Loading ->
|
is AsyncAction.Loading -> {
|
||||||
|
if (showProgressDialog) {
|
||||||
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
||||||
|
}
|
||||||
|
}
|
||||||
is AsyncAction.Failure ->
|
is AsyncAction.Failure ->
|
||||||
RetryDialog(
|
RetryDialog(
|
||||||
title = stringResource(id = CommonStrings.dialog_title_error),
|
title = stringResource(id = CommonStrings.dialog_title_error),
|
||||||
|
|||||||
+10
-1
@@ -20,7 +20,9 @@ import io.element.android.libraries.matrix.api.encryption.BackupState
|
|||||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||||
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.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
||||||
@@ -39,11 +41,18 @@ class LogoutPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createLogoutPresenter()
|
val presenter = createLogoutPresenter(
|
||||||
|
matrixClient = FakeMatrixClient(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
userDisplayName = "Alice",
|
||||||
|
userAvatarUrl = "mxc://avatar",
|
||||||
|
),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
|
assertThat(initialState.currentUser).isEqualTo(MatrixUser(A_SESSION_ID, "Alice", "mxc://avatar"))
|
||||||
assertThat(initialState.isLastDevice).isFalse()
|
assertThat(initialState.isLastDevice).isFalse()
|
||||||
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
|
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
|
||||||
assertThat(initialState.doesBackupExistOnServer).isTrue()
|
assertThat(initialState.doesBackupExistOnServer).isTrue()
|
||||||
|
|||||||
+32
-1
@@ -13,8 +13,11 @@ package io.element.android.features.logout.impl
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
import androidx.compose.ui.test.AndroidComposeUiTest
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
@@ -25,8 +28,22 @@ import io.element.android.tests.testutils.pressBack
|
|||||||
import io.element.android.tests.testutils.pressTag
|
import io.element.android.tests.testutils.pressTag
|
||||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
class LogoutViewTest : RobolectricTest() {
|
class LogoutViewTest : RobolectricTest() {
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
fun `the current user is displayed`() = runAndroidComposeUiTest {
|
||||||
|
setLogoutView(
|
||||||
|
aLogoutState(
|
||||||
|
currentUser = aMatrixUser(id = "@alice:server.org", displayName = "Alice"),
|
||||||
|
eventSink = EventsRecorder(expectEvents = false),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
onNodeWithText("Alice").assertIsDisplayed()
|
||||||
|
onNodeWithText("@alice:server.org").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on logout sends a LogoutEvent`() = runAndroidComposeUiTest {
|
fun `clicking on logout sends a LogoutEvent`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<LogoutEvent>()
|
val eventsRecorder = EventsRecorder<LogoutEvent>()
|
||||||
@@ -88,10 +105,24 @@ class LogoutViewTest : RobolectricTest() {
|
|||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
clickOn(CommonStrings.action_cancel)
|
clickOn(CommonStrings.action_cancel, inDialog = true)
|
||||||
eventsRecorder.assertSingle(LogoutEvent.CloseDialogs)
|
eventsRecorder.assertSingle(LogoutEvent.CloseDialogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on cancel invoke back callback`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<LogoutEvent>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
setLogoutView(
|
||||||
|
aLogoutState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackClick = callback,
|
||||||
|
)
|
||||||
|
clickOn(CommonStrings.action_cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest {
|
fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<LogoutEvent>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<LogoutEvent>(expectEvents = false)
|
||||||
|
|||||||
-44
@@ -16,13 +16,9 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.features.logout.api.direct.DirectLogoutEvent
|
import io.element.android.features.logout.api.direct.DirectLogoutEvent
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -38,44 +34,6 @@ class DirectLogoutPresenterTest {
|
|||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
assertThat(initialState.canDoDirectSignOut).isTrue()
|
|
||||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state - last session`() = runTest {
|
|
||||||
val presenter = createDirectLogoutPresenter(
|
|
||||||
encryptionService = FakeEncryptionService().apply {
|
|
||||||
emitIsLastDevice(true)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
|
||||||
assertThat(initialState.canDoDirectSignOut).isFalse()
|
|
||||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state - backing up`() = runTest {
|
|
||||||
val encryptionService = FakeEncryptionService()
|
|
||||||
encryptionService.givenWaitForBackupUploadSteadyStateFlow(
|
|
||||||
flow {
|
|
||||||
emit(BackupUploadState.Waiting)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val presenter = createDirectLogoutPresenter(
|
|
||||||
encryptionService = encryptionService
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
val initialState = awaitFirstItem()
|
|
||||||
assertThat(initialState.canDoDirectSignOut).isFalse()
|
|
||||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,9 +138,7 @@ class DirectLogoutPresenterTest {
|
|||||||
|
|
||||||
private fun createDirectLogoutPresenter(
|
private fun createDirectLogoutPresenter(
|
||||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
|
||||||
): DirectLogoutPresenter = DirectLogoutPresenter(
|
): DirectLogoutPresenter = DirectLogoutPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
encryptionService = encryptionService,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
-5
@@ -17,7 +17,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
@@ -34,7 +33,6 @@ class PreferencesAccountPresenter(
|
|||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val indicatorService: IndicatorService,
|
private val indicatorService: IndicatorService,
|
||||||
private val directLogoutPresenter: Presenter<DirectLogoutState>,
|
|
||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val sessionEnterpriseService: SessionEnterpriseService,
|
private val sessionEnterpriseService: SessionEnterpriseService,
|
||||||
@@ -65,8 +63,6 @@ class PreferencesAccountPresenter(
|
|||||||
canDeactivateAccount = matrixClient.canDeactivateAccount()
|
canDeactivateAccount = matrixClient.canDeactivateAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
val directLogoutState = directLogoutPresenter.present()
|
|
||||||
|
|
||||||
val accountManagementUrl by produceState<String?>(initialValue = null) {
|
val accountManagementUrl by produceState<String?>(initialValue = null) {
|
||||||
value = matrixClient.getAccountManagementUrl(null)
|
value = matrixClient.getAccountManagementUrl(null)
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
@@ -83,7 +79,6 @@ class PreferencesAccountPresenter(
|
|||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
showLinkNewDevice = showLinkNewDevice,
|
showLinkNewDevice = showLinkNewDevice,
|
||||||
canDeactivateAccount = canDeactivateAccount,
|
canDeactivateAccount = canDeactivateAccount,
|
||||||
directLogoutState = directLogoutState,
|
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
package io.element.android.features.preferences.impl.account
|
package io.element.android.features.preferences.impl.account
|
||||||
|
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
|
||||||
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.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
@@ -19,6 +18,5 @@ data class PreferencesAccountState(
|
|||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
val showLinkNewDevice: Boolean,
|
val showLinkNewDevice: Boolean,
|
||||||
val canDeactivateAccount: Boolean,
|
val canDeactivateAccount: Boolean,
|
||||||
val directLogoutState: DirectLogoutState,
|
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
)
|
)
|
||||||
|
|||||||
-4
@@ -8,8 +8,6 @@
|
|||||||
package io.element.android.features.preferences.impl.account
|
package io.element.android.features.preferences.impl.account
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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.designsystem.utils.snackbar.SnackbarMessage
|
||||||
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
|
||||||
@@ -56,7 +54,6 @@ fun aPreferencesAccountState(
|
|||||||
canReportBug: Boolean = false,
|
canReportBug: Boolean = false,
|
||||||
showLinkNewDevice: Boolean = false,
|
showLinkNewDevice: Boolean = false,
|
||||||
canDeactivateAccount: Boolean = false,
|
canDeactivateAccount: Boolean = false,
|
||||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
|
||||||
snackbarMessage: SnackbarMessage? = null,
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
) = PreferencesAccountState(
|
) = PreferencesAccountState(
|
||||||
myUser = myUser,
|
myUser = myUser,
|
||||||
@@ -66,6 +63,5 @@ fun aPreferencesAccountState(
|
|||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
showLinkNewDevice = showLinkNewDevice,
|
showLinkNewDevice = showLinkNewDevice,
|
||||||
canDeactivateAccount = canDeactivateAccount,
|
canDeactivateAccount = canDeactivateAccount,
|
||||||
directLogoutState = directLogoutState,
|
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-12
@@ -19,8 +19,6 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutEvent
|
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
|
||||||
import io.element.android.features.preferences.impl.account.PreferencesAccountCallback
|
import io.element.android.features.preferences.impl.account.PreferencesAccountCallback
|
||||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.architecture.callback
|
import io.element.android.libraries.architecture.callback
|
||||||
@@ -34,7 +32,6 @@ class PreferencesRootNode(
|
|||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: PreferencesRootPresenter,
|
private val presenter: PreferencesRootPresenter,
|
||||||
private val emojiPickerRenderer: EmojiPickerRenderer,
|
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
private val directLogoutView: DirectLogoutView,
|
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
interface Callback : PreferencesAccountCallback, Plugin {
|
interface Callback : PreferencesAccountCallback, Plugin {
|
||||||
fun navigateToAddAccount()
|
fun navigateToAddAccount()
|
||||||
@@ -89,16 +86,8 @@ class PreferencesRootNode(
|
|||||||
onManageAccountClick = { onManageAccountClick(activity, it, isDark) },
|
onManageAccountClick = { onManageAccountClick(activity, it, isDark) },
|
||||||
onOpenNotificationSettings = callback::navigateToNotificationSettings,
|
onOpenNotificationSettings = callback::navigateToNotificationSettings,
|
||||||
onEditProfileClick = callback::navigateToUserProfile,
|
onEditProfileClick = callback::navigateToUserProfile,
|
||||||
onSignOutClick = {
|
onSignOutClick = callback::startSignOutFlow,
|
||||||
if (state.preferencesAccountState.directLogoutState.canDoDirectSignOut) {
|
|
||||||
state.preferencesAccountState.directLogoutState.eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
|
|
||||||
} else {
|
|
||||||
callback.startSignOutFlow()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDeactivateClick = callback::startAccountDeactivationFlow
|
onDeactivateClick = callback::startAccountDeactivationFlow
|
||||||
)
|
)
|
||||||
|
|
||||||
directLogoutView.Render(state = state.preferencesAccountState.directLogoutState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -10,7 +10,6 @@ package io.element.android.features.preferences.impl.account
|
|||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
|
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
@@ -65,7 +64,6 @@ class PreferencesAccountPresenterTest {
|
|||||||
assertThat(initialState.showSecureBackupBadge).isFalse()
|
assertThat(initialState.showSecureBackupBadge).isFalse()
|
||||||
assertThat(initialState.accountManagementUrl).isNull()
|
assertThat(initialState.accountManagementUrl).isNull()
|
||||||
assertThat(initialState.showLinkNewDevice).isFalse()
|
assertThat(initialState.showLinkNewDevice).isFalse()
|
||||||
assertThat(initialState.directLogoutState).isEqualTo(aDirectLogoutState())
|
|
||||||
assertThat(initialState.snackbarMessage).isNull()
|
assertThat(initialState.snackbarMessage).isNull()
|
||||||
val loadedState = consumeItemsUntilPredicate { it.accountManagementUrl != null }.last()
|
val loadedState = consumeItemsUntilPredicate { it.accountManagementUrl != null }.last()
|
||||||
assertThat(loadedState.showSecureBackup).isFalse()
|
assertThat(loadedState.showSecureBackup).isFalse()
|
||||||
@@ -169,7 +167,6 @@ class PreferencesAccountPresenterTest {
|
|||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
indicatorService = indicatorService,
|
indicatorService = indicatorService,
|
||||||
directLogoutPresenter = { aDirectLogoutState() },
|
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
sessionEnterpriseService = sessionEnterpriseService,
|
sessionEnterpriseService = sessionEnterpriseService,
|
||||||
|
|||||||
+1
@@ -31,6 +31,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||||||
|
|
||||||
UserHeader(96.dp),
|
UserHeader(96.dp),
|
||||||
UserListItem(36.dp),
|
UserListItem(36.dp),
|
||||||
|
AccountInfoUser(36.dp),
|
||||||
|
|
||||||
SelectedUser(52.dp),
|
SelectedUser(52.dp),
|
||||||
SelectedRoom(56.dp),
|
SelectedRoom(56.dp),
|
||||||
|
|||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* 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.libraries.matrix.ui.components.accountinfo
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.TextWithLabelMolecule
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
|
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
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.Text
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=5873-996
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AccountInfoCard(
|
||||||
|
state: AccountInfoCardState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(1.dp, ElementTheme.colors.borderDisabled, RoundedCornerShape(8.dp))
|
||||||
|
.padding(20.dp),
|
||||||
|
) {
|
||||||
|
when (state.mode) {
|
||||||
|
AccountInfoCardMode.Simple -> Unit
|
||||||
|
is AccountInfoCardMode.DeviceVerification -> Unit
|
||||||
|
is AccountInfoCardMode.UserVerification -> {
|
||||||
|
Text(
|
||||||
|
// TODO
|
||||||
|
text = "User requesting verification:",
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AccountInfoCardUser(
|
||||||
|
matrixUser = state.matrixUser,
|
||||||
|
)
|
||||||
|
when (state.mode) {
|
||||||
|
AccountInfoCardMode.Simple -> Unit
|
||||||
|
is AccountInfoCardMode.DeviceVerification -> {
|
||||||
|
AccountInfoSeparator()
|
||||||
|
DeviceInfoRow(state.mode)
|
||||||
|
}
|
||||||
|
is AccountInfoCardMode.UserVerification -> {
|
||||||
|
AccountInfoSeparator()
|
||||||
|
AccountInfoCardUser(
|
||||||
|
matrixUser = state.mode.otherUser,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccountInfoSeparator() {
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(vertical = 20.dp),
|
||||||
|
color = ElementTheme.colors.separatorPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DeviceInfoRow(
|
||||||
|
mode: AccountInfoCardMode.DeviceVerification,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.semantics(mergeDescendants = true) {},
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
RoundedIconAtom(
|
||||||
|
modifier = Modifier,
|
||||||
|
size = RoundedIconAtomSize.Big,
|
||||||
|
resourceId = CompoundDrawables.ic_compound_devices
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = mode.deviceName ?: mode.deviceId.value,
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
TextWithLabelMolecule(
|
||||||
|
// TODO i18n
|
||||||
|
label = "Signed in",
|
||||||
|
text = mode.signInFormattedTimestamp,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
TextWithLabelMolecule(
|
||||||
|
label = stringResource(CommonStrings.common_device_id),
|
||||||
|
text = mode.deviceId.value,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
spellText = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccountInfoCardUser(matrixUser: MatrixUser) {
|
||||||
|
val subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Avatar(
|
||||||
|
avatarData = matrixUser.getAvatarData(AvatarSize.AccountInfoUser),
|
||||||
|
avatarType = AvatarType.User,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
// Name
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.clipToBounds(),
|
||||||
|
text = matrixUser.getBestName(),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
)
|
||||||
|
// Id
|
||||||
|
subtext?.let {
|
||||||
|
Text(
|
||||||
|
text = subtext,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun AccountInfoCardPreview(
|
||||||
|
@PreviewParameter(AccountInfoCardStatePreviewParam::class) state: AccountInfoCardState
|
||||||
|
) = ElementPreview {
|
||||||
|
// Add some padding to view the border
|
||||||
|
Box(modifier = Modifier.padding(16.dp)) {
|
||||||
|
AccountInfoCard(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.libraries.matrix.ui.components.accountinfo
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
|
data class AccountInfoCardState(
|
||||||
|
val matrixUser: MatrixUser,
|
||||||
|
val mode: AccountInfoCardMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface AccountInfoCardMode {
|
||||||
|
data object Simple : AccountInfoCardMode
|
||||||
|
|
||||||
|
data class UserVerification(
|
||||||
|
val otherUser: MatrixUser,
|
||||||
|
) : AccountInfoCardMode
|
||||||
|
|
||||||
|
// TODO Replace SessionDetailsView with this component
|
||||||
|
data class DeviceVerification(
|
||||||
|
val deviceName: String?,
|
||||||
|
val deviceId: DeviceId,
|
||||||
|
val signInFormattedTimestamp: String,
|
||||||
|
) : AccountInfoCardMode
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.libraries.matrix.ui.components.accountinfo
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
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
|
||||||
|
|
||||||
|
open class AccountInfoCardStatePreviewParam : PreviewParameterProvider<AccountInfoCardState> {
|
||||||
|
override val values: Sequence<AccountInfoCardState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
anAccountInfoCardState(),
|
||||||
|
anAccountInfoCardState(aMatrixUser(displayName = null)),
|
||||||
|
anAccountInfoCardState(
|
||||||
|
mode = AccountInfoCardMode.UserVerification(
|
||||||
|
otherUser = aMatrixUser(displayName = "Other User"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
anAccountInfoCardState(
|
||||||
|
mode = AccountInfoCardMode.DeviceVerification(
|
||||||
|
deviceName = "My Device",
|
||||||
|
deviceId = DeviceId("DEVICEID"),
|
||||||
|
signInFormattedTimestamp = "2024-06-01 12:00:00",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anAccountInfoCardState(
|
||||||
|
matrixUser: MatrixUser = aMatrixUser(),
|
||||||
|
mode: AccountInfoCardMode = AccountInfoCardMode.Simple,
|
||||||
|
) = AccountInfoCardState(
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
mode = mode,
|
||||||
|
)
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user