mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Show the account receiving the device verification request
Replace SessionDetailsView by AccountInfoCard with the DeviceVerification mode on the incoming device verification screen, so that the account concerned by the request is clearly indicated, with its avatar and Matrix ID. The current user is built from the SessionStore, including the locally stored avatar thumbnail, used as a fallback when the avatar cannot be loaded, for instance for an account which is not the current one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ dependencies {
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.logout.api)
|
||||
api(libs.statemachine)
|
||||
@@ -44,4 +45,5 @@ dependencies {
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
}
|
||||
|
||||
+24
@@ -13,6 +13,7 @@ package io.element.android.features.verifysession.impl.incoming
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -26,13 +27,17 @@ import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
@@ -45,6 +50,8 @@ class IncomingVerificationPresenter(
|
||||
@Assisted private val navigator: IncomingVerificationNavigator,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
private val sessionVerificationService: SessionVerificationService,
|
||||
private val sessionId: SessionId,
|
||||
private val sessionStore: SessionStore,
|
||||
private val stateMachine: IncomingVerificationStateMachine,
|
||||
private val dateFormatter: DateFormatter,
|
||||
) : Presenter<IncomingVerificationState> {
|
||||
@@ -92,9 +99,24 @@ class IncomingVerificationPresenter(
|
||||
mode = DateFormatterMode.TimeOrDate,
|
||||
)
|
||||
}
|
||||
val currentUser by remember {
|
||||
sessionStore.sessionsFlow().mapNotNull { sessions ->
|
||||
sessions.find { it.userId == sessionId.value }?.let { sessionData ->
|
||||
MatrixUser(
|
||||
userId = sessionId,
|
||||
displayName = sessionData.userDisplayName,
|
||||
avatarUrl = sessionData.userAvatarUrl,
|
||||
// Locally stored avatar, used as a fallback when the avatar cannot be loaded,
|
||||
// for instance for a session which is not the current one
|
||||
avatarThumbnail = sessionData.userAvatarData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.collectAsState(MatrixUser(sessionId))
|
||||
val step by remember {
|
||||
derivedStateOf {
|
||||
stateAndDispatch.state.value.toVerificationStep(
|
||||
currentUser = currentUser,
|
||||
sessionVerificationRequestDetails = verificationRequest.details,
|
||||
formattedSignInTime = formattedSignInTime,
|
||||
)
|
||||
@@ -147,6 +169,7 @@ class IncomingVerificationPresenter(
|
||||
}
|
||||
|
||||
private fun StateMachineState?.toVerificationStep(
|
||||
currentUser: MatrixUser,
|
||||
sessionVerificationRequestDetails: SessionVerificationRequestDetails,
|
||||
formattedSignInTime: String,
|
||||
): Step =
|
||||
@@ -156,6 +179,7 @@ class IncomingVerificationPresenter(
|
||||
StateMachineState.RejectingIncomingVerification,
|
||||
null -> {
|
||||
Step.Initial(
|
||||
currentUser = currentUser,
|
||||
deviceDisplayName = sessionVerificationRequestDetails.deviceDisplayName,
|
||||
deviceId = sessionVerificationRequestDetails.deviceId,
|
||||
formattedSignInTime = formattedSignInTime,
|
||||
|
||||
+2
@@ -10,6 +10,7 @@ package io.element.android.features.verifysession.impl.incoming
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
|
||||
@@ -21,6 +22,7 @@ data class IncomingVerificationState(
|
||||
@Stable
|
||||
sealed interface Step {
|
||||
data class Initial(
|
||||
val currentUser: MatrixUser,
|
||||
val deviceDisplayName: String?,
|
||||
val deviceId: DeviceId,
|
||||
val formattedSignInTime: String,
|
||||
|
||||
+6
@@ -13,6 +13,7 @@ import io.element.android.features.verifysession.impl.incoming.IncomingVerificat
|
||||
import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData
|
||||
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
|
||||
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
||||
import io.element.android.libraries.designsystem.preview.USER_NAME_BOB
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.FlowId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
@@ -50,6 +51,11 @@ open class IncomingVerificationStatePreviewParam : PreviewParameterProvider<Inco
|
||||
internal fun aStepInitial(
|
||||
isWaiting: Boolean = false,
|
||||
) = Step.Initial(
|
||||
currentUser = MatrixUser(
|
||||
userId = UserId("@bob:example.com"),
|
||||
displayName = USER_NAME_BOB,
|
||||
avatarUrl = null,
|
||||
),
|
||||
deviceDisplayName = "Element X Android",
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
formattedSignInTime = "12:34",
|
||||
|
||||
+12
-5
@@ -31,7 +31,6 @@ import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.verifysession.impl.R
|
||||
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
|
||||
import io.element.android.features.verifysession.impl.incoming.ui.SessionDetailsView
|
||||
import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu
|
||||
import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
|
||||
import io.element.android.features.verifysession.impl.ui.VerificationUserProfileContent
|
||||
@@ -47,6 +46,9 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
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.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
@@ -173,10 +175,15 @@ private fun ContentInitial(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
SessionDetailsView(
|
||||
deviceName = stepInitial.deviceDisplayName,
|
||||
deviceId = stepInitial.deviceId,
|
||||
signInFormattedTimestamp = stepInitial.formattedSignInTime,
|
||||
AccountInfoCard(
|
||||
state = AccountInfoCardState(
|
||||
matrixUser = stepInitial.currentUser,
|
||||
mode = AccountInfoCardMode.DeviceVerification(
|
||||
deviceName = stepInitial.deviceDisplayName,
|
||||
deviceId = stepInitial.deviceId,
|
||||
signInFormattedTimestamp = stepInitial.formattedSignInTime,
|
||||
),
|
||||
),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.verifysession.impl.incoming.ui
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.verifysession.impl.R
|
||||
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.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.Text
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun SessionDetailsView(
|
||||
deviceName: String?,
|
||||
deviceId: DeviceId,
|
||||
signInFormattedTimestamp: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = ElementTheme.colors.borderDisabled,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.padding(24.dp)
|
||||
.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 = deviceName ?: deviceId.value,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TextWithLabelMolecule(
|
||||
label = stringResource(R.string.screen_session_verification_request_details_timestamp),
|
||||
text = signInFormattedTimestamp,
|
||||
modifier = Modifier.weight(2f),
|
||||
)
|
||||
TextWithLabelMolecule(
|
||||
label = stringResource(CommonStrings.common_device_id),
|
||||
text = deviceId.value,
|
||||
modifier = Modifier.weight(5f),
|
||||
spellText = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SessionDetailsViewPreview() = ElementPreview {
|
||||
Column {
|
||||
SessionDetailsView(
|
||||
deviceName = "Element X Android",
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
signInFormattedTimestamp = "12:34",
|
||||
)
|
||||
SessionDetailsView(
|
||||
deviceName = null,
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
signInFormattedTimestamp = "12:34",
|
||||
)
|
||||
}
|
||||
}
|
||||
+46
@@ -18,10 +18,16 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationR
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_TIMESTAMP
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
@@ -41,6 +47,39 @@ class IncomingVerificationPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - the current user is built from the stored session`() = runTest {
|
||||
createPresenter(
|
||||
service = FakeSessionVerificationService(
|
||||
acknowledgeVerificationRequestLambda = { },
|
||||
resetLambda = { },
|
||||
),
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_SESSION_ID.value,
|
||||
userDisplayName = A_USER_NAME,
|
||||
userAvatarUrl = AN_AVATAR_URL,
|
||||
userAvatarData = "avatarData",
|
||||
)
|
||||
),
|
||||
),
|
||||
).test {
|
||||
val step = awaitItem().step as IncomingVerificationState.Step.Initial
|
||||
assertThat(step.currentUser).isEqualTo(MatrixUser(A_SESSION_ID))
|
||||
val stepWithUser = awaitItem().step as IncomingVerificationState.Step.Initial
|
||||
assertThat(stepWithUser.currentUser).isEqualTo(
|
||||
MatrixUser(
|
||||
userId = A_SESSION_ID,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
avatarThumbnail = "avatarData",
|
||||
)
|
||||
)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - nominal case - incoming verification successful`() = runTest {
|
||||
val acknowledgeVerificationRequestLambda = lambdaRecorder<VerificationRequest.Incoming, Unit> { _ -> }
|
||||
@@ -59,6 +98,7 @@ class IncomingVerificationPresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.step).isEqualTo(
|
||||
IncomingVerificationState.Step.Initial(
|
||||
currentUser = MatrixUser(A_SESSION_ID),
|
||||
deviceDisplayName = "a device name",
|
||||
deviceId = A_DEVICE_ID,
|
||||
formattedSignInTime = "567 TimeOrDate false",
|
||||
@@ -128,6 +168,7 @@ class IncomingVerificationPresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.step).isEqualTo(
|
||||
IncomingVerificationState.Step.Initial(
|
||||
currentUser = MatrixUser(A_SESSION_ID),
|
||||
deviceDisplayName = "a device name",
|
||||
deviceId = A_DEVICE_ID,
|
||||
formattedSignInTime = "567 TimeOrDate false",
|
||||
@@ -193,6 +234,7 @@ class IncomingVerificationPresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.step).isEqualTo(
|
||||
IncomingVerificationState.Step.Initial(
|
||||
currentUser = MatrixUser(A_SESSION_ID),
|
||||
deviceDisplayName = "a device name",
|
||||
deviceId = A_DEVICE_ID,
|
||||
formattedSignInTime = "567 TimeOrDate false",
|
||||
@@ -228,6 +270,7 @@ class IncomingVerificationPresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.step).isEqualTo(
|
||||
IncomingVerificationState.Step.Initial(
|
||||
currentUser = MatrixUser(A_SESSION_ID),
|
||||
deviceDisplayName = "a device name",
|
||||
deviceId = A_DEVICE_ID,
|
||||
formattedSignInTime = "567 TimeOrDate false",
|
||||
@@ -315,10 +358,13 @@ internal fun TestScope.createPresenter(
|
||||
navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() },
|
||||
service: SessionVerificationService = FakeSessionVerificationService(),
|
||||
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
) = IncomingVerificationPresenter(
|
||||
verificationRequest = verificationRequest,
|
||||
navigator = navigator,
|
||||
sessionVerificationService = service,
|
||||
sessionId = A_SESSION_ID,
|
||||
sessionStore = sessionStore,
|
||||
stateMachine = IncomingVerificationStateMachine(service),
|
||||
dateFormatter = dateFormatter,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
|
||||
Reference in New Issue
Block a user