Implement user verification (#4294)
* Add support for starting verification of a user * Add support for replying to incoming user verification requests * Add reset recovery key button and previews to `ChooseSelfVerificationModeView` * Add 'Profile' item in room details screen * Update screenshots * Remove `showDeviceVerifiedScreen` parameter from `NavTarget.UseAnotherDevice` * Allow exiting the FTUE flow, which will close the app. The previous state will be restored when the app is reopened. * When outgoing verification fails, move to the `Canceled` state. Then, when resetting the state machine state also reset the verification service. --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
6f54de58f3
commit
b91933625c
145 changed files with 1662 additions and 830 deletions
|
|
@ -9,6 +9,7 @@ package io.element.android.features.roomdetails.impl
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
|
|
@ -21,6 +22,7 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appconfig.LearnMoreConfig
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||
|
|
@ -35,11 +37,13 @@ import io.element.android.features.roomdetails.impl.notificationsettings.RoomNot
|
|||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
|
||||
import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyFlowNode
|
||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.overlay.operation.hide
|
||||
import io.element.android.libraries.architecture.overlay.operation.show
|
||||
import io.element.android.libraries.designsystem.utils.OpenUrlInTabView
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -47,6 +51,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
|
@ -65,6 +70,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||
private val mediaGalleryEntryPoint: MediaGalleryEntryPoint,
|
||||
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
||||
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||
|
|
@ -118,6 +124,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data object SecurityAndPrivacy : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class VerifyUser(val userId: UserId) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -168,6 +177,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
backstack.push(NavTarget.SecurityAndPrivacy)
|
||||
}
|
||||
|
||||
override fun openDmUserProfile(userId: UserId) {
|
||||
backstack.push(NavTarget.RoomMemberDetails(userId))
|
||||
}
|
||||
|
||||
override fun onJoinCall() {
|
||||
val inputs = CallType.RoomCall(
|
||||
sessionId = room.sessionId,
|
||||
|
|
@ -224,6 +237,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
override fun onStartCall(dmRoomId: RoomId) {
|
||||
elementCallEntryPoint.startCall(CallType.RoomCall(roomId = dmRoomId, sessionId = room.sessionId))
|
||||
}
|
||||
|
||||
override fun onVerifyUser(userId: UserId) {
|
||||
backstack.push(NavTarget.VerifyUser(userId))
|
||||
}
|
||||
}
|
||||
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback)
|
||||
createNode<RoomMemberDetailsNode>(buildContext, plugins)
|
||||
|
|
@ -301,11 +318,37 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
NavTarget.SecurityAndPrivacy -> {
|
||||
createNode<SecurityAndPrivacyFlowNode>(buildContext)
|
||||
}
|
||||
is NavTarget.VerifyUser -> {
|
||||
val params = VerifySessionEntryPoint.Params(
|
||||
showDeviceVerifiedScreen = true,
|
||||
verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId,)
|
||||
)
|
||||
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(params)
|
||||
.callback(object : VerifySessionEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun onBack() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun onLearnMoreAboutEncryption() {
|
||||
learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val learnMoreUrl = mutableStateOf<String?>(null)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackWithOverlayBox(modifier)
|
||||
|
||||
OpenUrlInTabView(learnMoreUrl)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import im.vector.app.features.analytics.plan.MobileScreen
|
|||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -50,6 +51,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
fun openPinnedMessagesList()
|
||||
fun openKnockRequestsList()
|
||||
fun openSecurityAndPrivacy()
|
||||
fun openDmUserProfile(userId: UserId)
|
||||
fun onJoinCall()
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +128,10 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
callbacks.forEach { it.openSecurityAndPrivacy() }
|
||||
}
|
||||
|
||||
private fun onProfileClick(userId: UserId) {
|
||||
callbacks.forEach { it.openDmUserProfile(userId) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -158,7 +164,8 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
onJoinCallClick = ::onJoinCall,
|
||||
onPinnedMessagesClick = ::openPinnedMessages,
|
||||
onKnockRequestsClick = ::openKnockRequestsLists,
|
||||
onSecurityAndPrivacyClick = ::openSecurityAndPrivacy
|
||||
onSecurityAndPrivacyClick = ::openSecurityAndPrivacy,
|
||||
onProfileClick = ::onProfileClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.getBestName
|
||||
|
|
@ -101,6 +102,7 @@ fun RoomDetailsView(
|
|||
onPinnedMessagesClick: () -> Unit,
|
||||
onKnockRequestsClick: () -> Unit,
|
||||
onSecurityAndPrivacyClick: () -> Unit,
|
||||
onProfileClick: (UserId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -179,11 +181,16 @@ fun RoomDetailsView(
|
|||
state.eventSink(RoomDetailsEvent.SetFavorite(it))
|
||||
}
|
||||
)
|
||||
|
||||
if (state.canShowSecurityAndPrivacy) {
|
||||
SecurityAndPrivacyItem(
|
||||
onClick = onSecurityAndPrivacyClick
|
||||
)
|
||||
}
|
||||
|
||||
state.roomMemberDetailsState?.let { dmMemberDetails ->
|
||||
ProfileItem(onClick = { onProfileClick(dmMemberDetails.userId) })
|
||||
}
|
||||
}
|
||||
|
||||
if (state.roomType is RoomDetailsType.Room) {
|
||||
|
|
@ -546,6 +553,17 @@ private fun FavoriteItem(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
|
||||
headlineContent = { Text(stringResource(id = R.string.screen_room_details_profile_row_title)) },
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MembersItem(
|
||||
memberCount: Long,
|
||||
|
|
@ -655,5 +673,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
|||
onPinnedMessagesClick = {},
|
||||
onKnockRequestsClick = {},
|
||||
onSecurityAndPrivacyClick = {},
|
||||
onProfileClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ class RoomMemberDetailsNode @AssistedInject constructor(
|
|||
onOpenDm = ::onStartDM,
|
||||
onStartCall = ::onStartCall,
|
||||
openAvatarPreview = callback::openAvatarPreview,
|
||||
onVerifyClick = callback::onVerifyUser,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ import androidx.compose.ui.test.onNodeWithTag
|
|||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureCalledOnceWithTwoParams
|
||||
|
|
@ -294,6 +297,21 @@ class RoomDetailsViewTest {
|
|||
rule.clickOn(R.string.screen_room_details_requests_to_join_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `click on profile invokes the expected callback`() {
|
||||
ensureCalledOnceWithParam(A_USER_ID) { callback ->
|
||||
rule.setRoomDetailView(
|
||||
state = aRoomDetailsState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
roomMemberDetailsState = aUserProfileState(userId = A_USER_ID),
|
||||
),
|
||||
onProfileClick = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_details_profile_row_title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDetailView(
|
||||
|
|
@ -314,6 +332,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
|
|||
onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
|
||||
onKnockRequestsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(),
|
||||
onProfileClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
RoomDetailsView(
|
||||
|
|
@ -332,6 +351,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
|
|||
onPinnedMessagesClick = onPinnedMessagesClick,
|
||||
onKnockRequestsClick = onKnockRequestsClick,
|
||||
onSecurityAndPrivacyClick = onSecurityAndPrivacyClick,
|
||||
onProfileClick = onProfileClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue