Move session verification to FTUE flow, make it mandatory (#2594)
* Move session verification to the FTUE * Allow session verification flow to be restarted * Use `EncryptionService` to display session verification faster * Remove session verification item from settings * Remove session verification banner from room list * Remove 'verification needed' variant from the `TimelineEncryptedHistoryBanner` * Improve verification flow UI and UX * Remove 'verification successful' snackbar message * Only register push provider after the session has been verified * Hide room list while the session hasn't been verified * Prevent deep links from changing the navigation if the session isn't verified * Update screenshots * Renamed `FtueState` to `FtueService`, created an actual `FtueState`. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
06ba5eaafc
commit
8b335a9125
198 changed files with 822 additions and 761 deletions
|
|
@ -23,7 +23,8 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- inputText: ${MAESTRO_PASSWORD}
|
- inputText: ${MAESTRO_PASSWORD}
|
||||||
- pressKey: Enter
|
- pressKey: Enter
|
||||||
- tapOn: "Continue"
|
- tapOn: "Continue"
|
||||||
|
- runFlow: ../assertions/assertSessionVerificationDisplayed.yaml
|
||||||
|
- runFlow: ./verifySession.yaml
|
||||||
- runFlow: ../assertions/assertAnalyticsDisplayed.yaml
|
- runFlow: ../assertions/assertAnalyticsDisplayed.yaml
|
||||||
- tapOn: "Not now"
|
- tapOn: "Not now"
|
||||||
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
||||||
- runFlow: ./verifySession.yaml
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
appId: ${MAESTRO_APP_ID}
|
appId: ${MAESTRO_APP_ID}
|
||||||
---
|
---
|
||||||
- tapOn: "Continue"
|
|
||||||
- takeScreenshot: build/maestro/150-Verify
|
- takeScreenshot: build/maestro/150-Verify
|
||||||
- tapOn: "Enter recovery key"
|
- tapOn: "Enter recovery key"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|
@ -8,4 +7,3 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- inputText: ${MAESTRO_RECOVERY_KEY}
|
- inputText: ${MAESTRO_RECOVERY_KEY}
|
||||||
- hideKeyboard
|
- hideKeyboard
|
||||||
- tapOn: "Confirm"
|
- tapOn: "Confirm"
|
||||||
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
appId: ${MAESTRO_APP_ID}
|
||||||
|
---
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible: "Confirm that it's you"
|
||||||
|
timeout: 10000
|
||||||
|
|
@ -19,8 +19,6 @@ package io.element.android.appnav
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
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.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -34,16 +32,12 @@ import javax.inject.Inject
|
||||||
class LoggedInEventProcessor @Inject constructor(
|
class LoggedInEventProcessor @Inject constructor(
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
roomMembershipObserver: RoomMembershipObserver,
|
roomMembershipObserver: RoomMembershipObserver,
|
||||||
sessionVerificationService: SessionVerificationService,
|
|
||||||
) {
|
) {
|
||||||
private var observingJob: Job? = null
|
private var observingJob: Job? = null
|
||||||
|
|
||||||
private val displayLeftRoomMessage = roomMembershipObserver.updates
|
private val displayLeftRoomMessage = roomMembershipObserver.updates
|
||||||
.map { !it.isUserInRoom }
|
.map { !it.isUserInRoom }
|
||||||
|
|
||||||
private val displayVerificationSuccessfulMessage = sessionVerificationService.verificationFlowState
|
|
||||||
.map { it == VerificationFlowState.Finished }
|
|
||||||
|
|
||||||
fun observeEvents(coroutineScope: CoroutineScope) {
|
fun observeEvents(coroutineScope: CoroutineScope) {
|
||||||
observingJob = coroutineScope.launch {
|
observingJob = coroutineScope.launch {
|
||||||
displayLeftRoomMessage
|
displayLeftRoomMessage
|
||||||
|
|
@ -52,13 +46,6 @@ class LoggedInEventProcessor @Inject constructor(
|
||||||
displayMessage(CommonStrings.common_current_user_left_room)
|
displayMessage(CommonStrings.common_current_user_left_room)
|
||||||
}
|
}
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
|
|
||||||
displayVerificationSuccessfulMessage
|
|
||||||
.filter { it }
|
|
||||||
.onEach {
|
|
||||||
displayMessage(CommonStrings.common_verification_complete)
|
|
||||||
}
|
|
||||||
.launchIn(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import io.element.android.appnav.room.RoomFlowNode
|
||||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
||||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
|
|
@ -56,13 +57,13 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||||
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.waitForChildAttached
|
import io.element.android.libraries.architecture.waitForChildAttached
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||||
|
|
@ -75,6 +76,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
@ -88,14 +91,13 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val preferencesEntryPoint: PreferencesEntryPoint,
|
private val preferencesEntryPoint: PreferencesEntryPoint,
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
|
||||||
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||||
private val inviteListEntryPoint: InviteListEntryPoint,
|
private val inviteListEntryPoint: InviteListEntryPoint,
|
||||||
private val ftueEntryPoint: FtueEntryPoint,
|
private val ftueEntryPoint: FtueEntryPoint,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
private val ftueState: FtueState,
|
private val ftueService: FtueService,
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||||
private val lockScreenStateService: LockScreenService,
|
private val lockScreenStateService: LockScreenService,
|
||||||
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
|
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
|
||||||
|
|
@ -103,7 +105,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.RoomList,
|
initialElement = NavTarget.Placeholder,
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
permanentNavModel = PermanentNavModel(
|
permanentNavModel = PermanentNavModel(
|
||||||
|
|
@ -121,7 +123,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||||
snackbarDispatcher,
|
snackbarDispatcher,
|
||||||
matrixClient.roomMembershipObserver(),
|
matrixClient.roomMembershipObserver(),
|
||||||
matrixClient.sessionVerificationService(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -133,9 +134,15 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
||||||
loggedInFlowProcessor.observeEvents(coroutineScope)
|
loggedInFlowProcessor.observeEvents(coroutineScope)
|
||||||
|
|
||||||
if (ftueState.shouldDisplayFlow.value) {
|
ftueService.state
|
||||||
backstack.push(NavTarget.Ftue)
|
.onEach { ftueState ->
|
||||||
}
|
when (ftueState) {
|
||||||
|
is FtueState.Unknown -> Unit // Nothing to do
|
||||||
|
is FtueState.Incomplete -> backstack.safeRoot(NavTarget.Ftue)
|
||||||
|
is FtueState.Complete -> backstack.safeRoot(NavTarget.RoomList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
},
|
},
|
||||||
onStop = {
|
onStop = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
@ -191,6 +198,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data object Placeholder : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object LoggedInPermanent : NavTarget
|
data object LoggedInPermanent : NavTarget
|
||||||
|
|
||||||
|
|
@ -214,9 +224,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object CreateRoom : NavTarget
|
data object CreateRoom : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object VerifySession : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SecureBackup(
|
data class SecureBackup(
|
||||||
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
|
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
|
||||||
|
|
@ -234,6 +241,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
|
NavTarget.Placeholder -> createNode<PlaceholderNode>(buildContext)
|
||||||
NavTarget.LoggedInPermanent -> {
|
NavTarget.LoggedInPermanent -> {
|
||||||
createNode<LoggedInNode>(buildContext)
|
createNode<LoggedInNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
|
@ -256,10 +264,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.CreateRoom)
|
backstack.push(NavTarget.CreateRoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionVerificationClicked() {
|
|
||||||
backstack.push(NavTarget.VerifySession)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSessionConfirmRecoveryKeyClicked() {
|
override fun onSessionConfirmRecoveryKeyClicked() {
|
||||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||||
}
|
}
|
||||||
|
|
@ -308,10 +312,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVerifyClicked() {
|
|
||||||
backstack.push(NavTarget.VerifySession)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSecureBackupClicked() {
|
override fun onSecureBackupClicked() {
|
||||||
backstack.push(NavTarget.SecureBackup())
|
backstack.push(NavTarget.SecureBackup())
|
||||||
}
|
}
|
||||||
|
|
@ -338,25 +338,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.VerifySession -> {
|
|
||||||
val callback = object : VerifySessionEntryPoint.Callback {
|
|
||||||
override fun onEnterRecoveryKey() {
|
|
||||||
backstack.replace(
|
|
||||||
NavTarget.SecureBackup(
|
|
||||||
initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDone() {
|
|
||||||
backstack.pop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verifySessionEntryPoint
|
|
||||||
.nodeBuilder(this, buildContext)
|
|
||||||
.callback(callback)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
is NavTarget.SecureBackup -> {
|
is NavTarget.SecureBackup -> {
|
||||||
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
||||||
|
|
@ -381,7 +362,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
ftueEntryPoint.nodeBuilder(this, buildContext)
|
ftueEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.callback(object : FtueEntryPoint.Callback {
|
.callback(object : FtueEntryPoint.Callback {
|
||||||
override fun onFtueFlowFinished() {
|
override fun onFtueFlowFinished() {
|
||||||
backstack.pop()
|
lifecycleScope.launch { attachRoomList() }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -398,20 +379,23 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun attachRoot(): Node {
|
suspend fun attachRoomList() {
|
||||||
return attachChild {
|
if (!canShowRoomList()) return
|
||||||
|
attachChild<Node> {
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun attachRoom(roomId: RoomId): RoomFlowNode {
|
suspend fun attachRoom(roomId: RoomId) {
|
||||||
return attachChild {
|
if (!canShowRoomList()) return
|
||||||
|
attachChild<RoomFlowNode> {
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
|
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
|
||||||
|
if (!canShowRoomList()) return@withContext
|
||||||
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
backstack.push(NavTarget.InviteList)
|
backstack.push(NavTarget.InviteList)
|
||||||
|
|
@ -420,13 +404,17 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun canShowRoomList(): Boolean {
|
||||||
|
return ftueService.state.value is FtueState.Complete
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
||||||
|
val isFtueDisplayed by ftueService.state.collectAsState()
|
||||||
BackstackView()
|
BackstackView()
|
||||||
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
if (isFtueDisplayed is FtueState.Complete) {
|
||||||
if (!isFtueDisplayed) {
|
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||||
}
|
}
|
||||||
if (lockScreenState == LockScreenLockState.Locked) {
|
if (lockScreenState == LockScreenLockState.Locked) {
|
||||||
|
|
@ -434,4 +422,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ContributesNode(AppScope::class)
|
||||||
|
class PlaceholderNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
) : Node(buildContext, plugins = plugins)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
.attachSession()
|
.attachSession()
|
||||||
.apply {
|
.apply {
|
||||||
when (deeplinkData) {
|
when (deeplinkData) {
|
||||||
is DeeplinkData.Root -> attachRoot()
|
is DeeplinkData.Root -> attachRoomList()
|
||||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
||||||
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,22 +27,32 @@ import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LoggedInPresenter @Inject constructor(
|
class LoggedInPresenter @Inject constructor(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val pushService: PushService,
|
private val pushService: PushService,
|
||||||
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
) : Presenter<LoggedInState> {
|
) : Presenter<LoggedInState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): LoggedInState {
|
override fun present(): LoggedInState {
|
||||||
LaunchedEffect(Unit) {
|
val isVerified by remember {
|
||||||
// Ensure pusher is registered
|
sessionVerificationService.sessionVerifiedStatus.map { it == SessionVerifiedStatus.Verified }
|
||||||
// TODO Manually select push provider for now
|
}.collectAsState(initial = false)
|
||||||
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
|
|
||||||
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
LaunchedEffect(isVerified) {
|
||||||
pushService.registerWith(matrixClient, pushProvider, distributor)
|
if (isVerified) {
|
||||||
|
// Ensure pusher is registered
|
||||||
|
// TODO Manually select push provider for now
|
||||||
|
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
|
||||||
|
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
||||||
|
pushService.registerWith(matrixClient, pushProvider, distributor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
|
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
|
|
@ -71,6 +72,7 @@ class LoggedInPresenterTest {
|
||||||
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
||||||
networkMonitor = FakeNetworkMonitor(networkStatus),
|
networkMonitor = FakeNetworkMonitor(networkStatus),
|
||||||
pushService = FakePushService(),
|
pushService = FakePushService(),
|
||||||
|
sessionVerificationService = FakeSessionVerificationService(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
changelog.d/2580.feature
Normal file
1
changelog.d/2580.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Move session verification to the after login flow and make it mandatory.
|
||||||
|
|
@ -18,8 +18,25 @@ package io.element.android.features.ftue.api.state
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface FtueState {
|
/**
|
||||||
val shouldDisplayFlow: StateFlow<Boolean>
|
* Service to manage the First Time User Experience state (aka Onboarding).
|
||||||
|
*/
|
||||||
|
interface FtueService {
|
||||||
|
/** The current state of the FTUE. */
|
||||||
|
val state: StateFlow<FtueState>
|
||||||
|
|
||||||
|
/** Reset the FTUE state. */
|
||||||
suspend fun reset()
|
suspend fun reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The state of the FTUE. */
|
||||||
|
sealed interface FtueState {
|
||||||
|
/** The FTUE state is unknown, nothing to do for now. */
|
||||||
|
data object Unknown : FtueState
|
||||||
|
|
||||||
|
/** The FTUE state is incomplete. The FTUE flow should be displayed. */
|
||||||
|
data object Incomplete : FtueState
|
||||||
|
|
||||||
|
/** The FTUE state is complete. The FTUE flow should not be displayed anymore. */
|
||||||
|
data object Complete : FtueState
|
||||||
|
}
|
||||||
|
|
@ -42,6 +42,8 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.features.analytics.api)
|
implementation(projects.features.analytics.api)
|
||||||
|
implementation(projects.features.securebackup.api)
|
||||||
|
implementation(projects.features.verifysession.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.features.lockscreen.api)
|
implementation(projects.features.lockscreen.api)
|
||||||
implementation(projects.libraries.permissions.api)
|
implementation(projects.libraries.permissions.api)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
|
||||||
|
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
|
@ -55,7 +56,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
class FtueFlowNode @AssistedInject constructor(
|
class FtueFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val ftueState: DefaultFtueState,
|
private val ftueState: DefaultFtueService,
|
||||||
private val analyticsEntryPoint: AnalyticsEntryPoint,
|
private val analyticsEntryPoint: AnalyticsEntryPoint,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||||
|
|
@ -72,6 +73,9 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Placeholder : NavTarget
|
data object Placeholder : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object SessionVerification : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object NotificationsOptIn : NavTarget
|
data object NotificationsOptIn : NavTarget
|
||||||
|
|
||||||
|
|
@ -106,6 +110,14 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
NavTarget.Placeholder -> {
|
NavTarget.Placeholder -> {
|
||||||
createNode<PlaceholderNode>(buildContext)
|
createNode<PlaceholderNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
NavTarget.SessionVerification -> {
|
||||||
|
val callback = object : FtueSessionVerificationFlowNode.Callback {
|
||||||
|
override fun onDone() {
|
||||||
|
lifecycleScope.launch { moveToNextStep() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<FtueSessionVerificationFlowNode>(buildContext, listOf(callback))
|
||||||
|
}
|
||||||
NavTarget.NotificationsOptIn -> {
|
NavTarget.NotificationsOptIn -> {
|
||||||
val callback = object : NotificationsOptInNode.Callback {
|
val callback = object : NotificationsOptInNode.Callback {
|
||||||
override fun onNotificationsOptInFinished() {
|
override fun onNotificationsOptInFinished() {
|
||||||
|
|
@ -133,6 +145,9 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
private fun moveToNextStep() {
|
private fun moveToNextStep() {
|
||||||
when (ftueState.getNextStep()) {
|
when (ftueState.getNextStep()) {
|
||||||
|
FtueStep.SessionVerification -> {
|
||||||
|
backstack.newRoot(NavTarget.SessionVerification)
|
||||||
|
}
|
||||||
FtueStep.NotificationsOptIn -> {
|
FtueStep.NotificationsOptIn -> {
|
||||||
backstack.newRoot(NavTarget.NotificationsOptIn)
|
backstack.newRoot(NavTarget.NotificationsOptIn)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.impl.sessionverification
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||||
|
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
||||||
|
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||||
|
) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>(
|
||||||
|
backstack = BackStack(
|
||||||
|
initialElement = NavTarget.Root,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
|
) {
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data object Root : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object EnterRecoveryKey : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callback = plugins<Callback>().first()
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
is NavTarget.Root -> {
|
||||||
|
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.callback(object : VerifySessionEntryPoint.Callback {
|
||||||
|
override fun onEnterRecoveryKey() {
|
||||||
|
backstack.push(NavTarget.EnterRecoveryKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDone() {
|
||||||
|
callback.onDone()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
is NavTarget.EnterRecoveryKey -> {
|
||||||
|
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||||
|
.callback(object : SecureBackupEntryPoint.Callback {
|
||||||
|
override fun onDone() {
|
||||||
|
callback.onDone()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
BackstackView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,9 +20,12 @@ import android.Manifest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
|
|
@ -35,14 +38,15 @@ import kotlinx.coroutines.runBlocking
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFtueState @Inject constructor(
|
class DefaultFtueService @Inject constructor(
|
||||||
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val permissionStateProvider: PermissionStateProvider,
|
private val permissionStateProvider: PermissionStateProvider,
|
||||||
private val lockScreenService: LockScreenService,
|
private val lockScreenService: LockScreenService,
|
||||||
) : FtueState {
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
|
) : FtueService {
|
||||||
|
override val state = MutableStateFlow<FtueState>(FtueState.Unknown)
|
||||||
|
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
analyticsService.reset()
|
analyticsService.reset()
|
||||||
|
|
@ -52,6 +56,10 @@ class DefaultFtueState @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
sessionVerificationService.sessionVerifiedStatus
|
||||||
|
.onEach { updateState() }
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
|
||||||
analyticsService.didAskUserConsent()
|
analyticsService.didAskUserConsent()
|
||||||
.onEach { updateState() }
|
.onEach { updateState() }
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
|
|
@ -59,7 +67,12 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||||
when (currentStep) {
|
when (currentStep) {
|
||||||
null -> if (shouldAskNotificationPermissions()) {
|
null -> if (isSessionNotVerified()) {
|
||||||
|
FtueStep.SessionVerification
|
||||||
|
} else {
|
||||||
|
getNextStep(FtueStep.SessionVerification)
|
||||||
|
}
|
||||||
|
FtueStep.SessionVerification -> if (shouldAskNotificationPermissions()) {
|
||||||
FtueStep.NotificationsOptIn
|
FtueStep.NotificationsOptIn
|
||||||
} else {
|
} else {
|
||||||
getNextStep(FtueStep.NotificationsOptIn)
|
getNextStep(FtueStep.NotificationsOptIn)
|
||||||
|
|
@ -79,12 +92,21 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
private fun isAnyStepIncomplete(): Boolean {
|
private fun isAnyStepIncomplete(): Boolean {
|
||||||
return listOf(
|
return listOf(
|
||||||
|
{ isSessionNotVerified() },
|
||||||
{ shouldAskNotificationPermissions() },
|
{ shouldAskNotificationPermissions() },
|
||||||
{ needsAnalyticsOptIn() },
|
{ needsAnalyticsOptIn() },
|
||||||
{ shouldDisplayLockscreenSetup() },
|
{ shouldDisplayLockscreenSetup() },
|
||||||
).any { it() }
|
).any { it() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isSessionVerificationServiceReady(): Boolean {
|
||||||
|
return sessionVerificationService.sessionVerifiedStatus.value != SessionVerifiedStatus.Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSessionNotVerified(): Boolean {
|
||||||
|
return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified
|
||||||
|
}
|
||||||
|
|
||||||
private fun needsAnalyticsOptIn(): Boolean {
|
private fun needsAnalyticsOptIn(): Boolean {
|
||||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
||||||
|
|
@ -109,11 +131,16 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun updateState() {
|
internal fun updateState() {
|
||||||
shouldDisplayFlow.value = isAnyStepIncomplete()
|
state.value = when {
|
||||||
|
!isSessionVerificationServiceReady() -> FtueState.Unknown
|
||||||
|
isAnyStepIncomplete() -> FtueState.Incomplete
|
||||||
|
else -> FtueState.Complete
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface FtueStep {
|
sealed interface FtueStep {
|
||||||
|
data object SessionVerification : FtueStep
|
||||||
data object NotificationsOptIn : FtueStep
|
data object NotificationsOptIn : FtueStep
|
||||||
data object AnalyticsOptIn : FtueStep
|
data object AnalyticsOptIn : FtueStep
|
||||||
data object LockscreenSetup : FtueStep
|
data object LockscreenSetup : FtueStep
|
||||||
|
|
@ -17,11 +17,15 @@
|
||||||
package io.element.android.features.ftue.impl
|
package io.element.android.features.ftue.impl
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
|
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
|
|
@ -32,38 +36,51 @@ import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DefaultFtueStateTests {
|
class DefaultFtueServiceTests {
|
||||||
@Test
|
@Test
|
||||||
fun `given any check being false, should display flow is true`() = runTest {
|
fun `given any check being false and session verification state being loaded, FtueState is Incomplete`() = runTest {
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService().apply {
|
||||||
|
givenVerifiedStatus(SessionVerifiedStatus.Unknown)
|
||||||
|
}
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val state = createState(coroutineScope)
|
val state = createState(coroutineScope, sessionVerificationService)
|
||||||
|
|
||||||
assertThat(state.shouldDisplayFlow.value).isTrue()
|
state.state.test {
|
||||||
|
// Verification state is unknown, we don't display the flow yet
|
||||||
|
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
|
||||||
|
|
||||||
|
// Verification state is known, we should display the flow if any check is false
|
||||||
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
|
assertThat(awaitItem()).isEqualTo(FtueState.Incomplete)
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given all checks being true, should display flow is false`() = runTest {
|
fun `given all checks being true, FtueState is Complete`() = runTest {
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService()
|
||||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
|
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
|
||||||
val lockScreenService = FakeLockScreenService()
|
val lockScreenService = FakeLockScreenService()
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
|
||||||
val state = createState(
|
val state = createState(
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
analyticsService.setDidAskUserConsent()
|
analyticsService.setDidAskUserConsent()
|
||||||
permissionStateProvider.setPermissionGranted()
|
permissionStateProvider.setPermissionGranted()
|
||||||
lockScreenService.setIsPinSetup(true)
|
lockScreenService.setIsPinSetup(true)
|
||||||
state.updateState()
|
state.updateState()
|
||||||
|
|
||||||
assertThat(state.shouldDisplayFlow.value).isFalse()
|
assertThat(state.state.value).isEqualTo(FtueState.Complete)
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
|
|
@ -71,6 +88,9 @@ class DefaultFtueStateTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `traverse flow`() = runTest {
|
fun `traverse flow`() = runTest {
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService().apply {
|
||||||
|
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
|
}
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||||
val lockScreenService = FakeLockScreenService()
|
val lockScreenService = FakeLockScreenService()
|
||||||
|
|
@ -78,12 +98,17 @@ class DefaultFtueStateTests {
|
||||||
|
|
||||||
val state = createState(
|
val state = createState(
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
)
|
)
|
||||||
val steps = mutableListOf<FtueStep?>()
|
val steps = mutableListOf<FtueStep?>()
|
||||||
|
|
||||||
|
// Session verification
|
||||||
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
|
|
||||||
// Notifications opt in
|
// Notifications opt in
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
permissionStateProvider.setPermissionGranted()
|
permissionStateProvider.setPermissionGranted()
|
||||||
|
|
@ -100,6 +125,7 @@ class DefaultFtueStateTests {
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
|
||||||
assertThat(steps).containsExactly(
|
assertThat(steps).containsExactly(
|
||||||
|
FtueStep.SessionVerification,
|
||||||
FtueStep.NotificationsOptIn,
|
FtueStep.NotificationsOptIn,
|
||||||
FtueStep.LockscreenSetup,
|
FtueStep.LockscreenSetup,
|
||||||
FtueStep.AnalyticsOptIn,
|
FtueStep.AnalyticsOptIn,
|
||||||
|
|
@ -114,17 +140,20 @@ class DefaultFtueStateTests {
|
||||||
@Test
|
@Test
|
||||||
fun `if a check for a step is true, start from the next one`() = runTest {
|
fun `if a check for a step is true, start from the next one`() = runTest {
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService()
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||||
val lockScreenService = FakeLockScreenService()
|
val lockScreenService = FakeLockScreenService()
|
||||||
val state = createState(
|
val state = createState(
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Skip first 2 steps
|
// Skip first 3 steps
|
||||||
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
permissionStateProvider.setPermissionGranted()
|
permissionStateProvider.setPermissionGranted()
|
||||||
lockScreenService.setIsPinSetup(true)
|
lockScreenService.setIsPinSetup(true)
|
||||||
|
|
||||||
|
|
@ -140,16 +169,19 @@ class DefaultFtueStateTests {
|
||||||
@Test
|
@Test
|
||||||
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest {
|
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest {
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService()
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val lockScreenService = FakeLockScreenService()
|
val lockScreenService = FakeLockScreenService()
|
||||||
|
|
||||||
val state = createState(
|
val state = createState(
|
||||||
sdkIntVersion = Build.VERSION_CODES.M,
|
sdkIntVersion = Build.VERSION_CODES.M,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
lockScreenService.setIsPinSetup(true)
|
lockScreenService.setIsPinSetup(true)
|
||||||
|
|
||||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||||
|
|
@ -163,14 +195,16 @@ class DefaultFtueStateTests {
|
||||||
|
|
||||||
private fun createState(
|
private fun createState(
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
|
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
||||||
lockScreenService: LockScreenService = FakeLockScreenService(),
|
lockScreenService: LockScreenService = FakeLockScreenService(),
|
||||||
// First version where notification permission is required
|
// First version where notification permission is required
|
||||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||||
) = DefaultFtueState(
|
) = DefaultFtueService(
|
||||||
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
|
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
|
|
@ -33,7 +33,6 @@ import io.element.android.features.messages.impl.MessagesNavigator
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.poll.api.actions.EndPollAction
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
|
|
@ -41,15 +40,11 @@ import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -68,8 +63,6 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val appScope: CoroutineScope,
|
private val appScope: CoroutineScope,
|
||||||
@Assisted private val navigator: MessagesNavigator,
|
@Assisted private val navigator: MessagesNavigator,
|
||||||
private val verificationService: SessionVerificationService,
|
|
||||||
private val encryptionService: EncryptionService,
|
|
||||||
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||||
private val sendPollResponseAction: SendPollResponseAction,
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
private val endPollAction: EndPollAction,
|
private val endPollAction: EndPollAction,
|
||||||
|
|
@ -101,21 +94,9 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
val newItemState = remember { mutableStateOf(NewEventState.None) }
|
val newItemState = remember { mutableStateOf(NewEventState.None) }
|
||||||
|
|
||||||
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
|
|
||||||
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
|
|
||||||
|
|
||||||
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
|
|
||||||
val sessionState by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
SessionState(
|
|
||||||
isSessionVerified = sessionVerifiedStatus == SessionVerifiedStatus.Verified,
|
|
||||||
isKeyBackupEnabled = keyBackupState == BackupState.ENABLED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleEvents(event: TimelineEvents) {
|
fun handleEvents(event: TimelineEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
||||||
|
|
@ -184,7 +165,6 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
newEventState = newItemState.value,
|
newEventState = newItemState.value,
|
||||||
sessionState = sessionState,
|
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ package io.element.android.features.messages.impl.timeline
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -32,7 +31,6 @@ data class TimelineState(
|
||||||
val highlightedEventId: EventId?,
|
val highlightedEventId: EventId?,
|
||||||
val paginationState: MatrixTimeline.PaginationState,
|
val paginationState: MatrixTimeline.PaginationState,
|
||||||
val newEventState: NewEventState,
|
val newEventState: NewEventState,
|
||||||
val sessionState: SessionState,
|
|
||||||
val eventSink: (TimelineEvents) -> Unit
|
val eventSink: (TimelineEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.session.aSessionState
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -58,10 +57,6 @@ fun aTimelineState(
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
highlightedEventId = null,
|
highlightedEventId = null,
|
||||||
newEventState = NewEventState.None,
|
newEventState = NewEventState.None,
|
||||||
sessionState = aSessionState(
|
|
||||||
isSessionVerified = true,
|
|
||||||
isKeyBackupEnabled = true,
|
|
||||||
),
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,6 @@ fun TimelineView(
|
||||||
onMoreReactionsClick = onMoreReactionsClicked,
|
onMoreReactionsClick = onMoreReactionsClicked,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
sessionState = state.sessionState,
|
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,6 @@ import io.element.android.features.messages.impl.timeline.components.group.Group
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.features.messages.impl.timeline.session.aSessionState
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -46,7 +44,6 @@ fun TimelineItemGroupedEventsRow(
|
||||||
renderReadReceipts: Boolean,
|
renderReadReceipts: Boolean,
|
||||||
isLastOutgoingMessage: Boolean,
|
isLastOutgoingMessage: Boolean,
|
||||||
highlightedItem: String?,
|
highlightedItem: String?,
|
||||||
sessionState: SessionState,
|
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
|
@ -74,7 +71,6 @@ fun TimelineItemGroupedEventsRow(
|
||||||
highlightedItem = highlightedItem,
|
highlightedItem = highlightedItem,
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
sessionState = sessionState,
|
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
|
|
@ -99,7 +95,6 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
highlightedItem: String?,
|
highlightedItem: String?,
|
||||||
renderReadReceipts: Boolean,
|
renderReadReceipts: Boolean,
|
||||||
isLastOutgoingMessage: Boolean,
|
isLastOutgoingMessage: Boolean,
|
||||||
sessionState: SessionState,
|
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
|
@ -133,7 +128,6 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
highlightedItem = highlightedItem,
|
highlightedItem = highlightedItem,
|
||||||
sessionState = sessionState,
|
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
|
|
@ -174,7 +168,6 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
|
||||||
highlightedItem = null,
|
highlightedItem = null,
|
||||||
renderReadReceipts = true,
|
renderReadReceipts = true,
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
sessionState = aSessionState(),
|
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
|
|
@ -200,7 +193,6 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
|
||||||
highlightedItem = null,
|
highlightedItem = null,
|
||||||
renderReadReceipts = true,
|
renderReadReceipts = true,
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
sessionState = aSessionState(),
|
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
|
@ -34,7 +33,6 @@ internal fun TimelineItemRow(
|
||||||
renderReadReceipts: Boolean,
|
renderReadReceipts: Boolean,
|
||||||
isLastOutgoingMessage: Boolean,
|
isLastOutgoingMessage: Boolean,
|
||||||
highlightedItem: String?,
|
highlightedItem: String?,
|
||||||
sessionState: SessionState,
|
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onLinkClicked: (String) -> Unit,
|
onLinkClicked: (String) -> Unit,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -53,7 +51,6 @@ internal fun TimelineItemRow(
|
||||||
is TimelineItem.Virtual -> {
|
is TimelineItem.Virtual -> {
|
||||||
TimelineItemVirtualRow(
|
TimelineItemVirtualRow(
|
||||||
virtual = timelineItem,
|
virtual = timelineItem,
|
||||||
sessionState = sessionState,
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +97,6 @@ internal fun TimelineItemRow(
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
highlightedItem = highlightedItem,
|
highlightedItem = highlightedItem,
|
||||||
sessionState = sessionState,
|
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,15 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemVirtualRow(
|
fun TimelineItemVirtualRow(
|
||||||
virtual: TimelineItem.Virtual,
|
virtual: TimelineItem.Virtual,
|
||||||
sessionState: SessionState,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
when (virtual.model) {
|
when (virtual.model) {
|
||||||
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
||||||
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
|
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
|
||||||
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(sessionState, modifier)
|
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.virtual
|
package io.element.android.features.messages.impl.timeline.components.virtual
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -29,20 +28,16 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.messages.impl.R
|
import io.element.android.features.messages.impl.R
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionStateProvider
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineEncryptedHistoryBannerView(
|
fun TimelineEncryptedHistoryBannerView(
|
||||||
sessionState: SessionState,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -61,26 +56,15 @@ fun TimelineEncryptedHistoryBannerView(
|
||||||
tint = ElementTheme.colors.iconInfoPrimary
|
tint = ElementTheme.colors.iconInfoPrimary
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(sessionState.toStringResId()),
|
text = stringResource(R.string.screen_room_encrypted_history_banner),
|
||||||
style = ElementTheme.typography.fontBodyMdMedium,
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
color = ElementTheme.colors.textInfoPrimary
|
color = ElementTheme.colors.textInfoPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@StringRes
|
|
||||||
private fun SessionState.toStringResId(): Int {
|
|
||||||
return when {
|
|
||||||
isSessionVerified.not() -> R.string.screen_room_encrypted_history_banner_unverified
|
|
||||||
isKeyBackupEnabled.not() -> R.string.screen_room_encrypted_history_banner
|
|
||||||
else -> R.string.screen_room_encrypted_history_banner // TODO strings need to be updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun EncryptedHistoryBannerViewPreview(
|
internal fun EncryptedHistoryBannerViewPreview() = ElementPreview {
|
||||||
@PreviewParameter(SessionStateProvider::class) sessionState: SessionState,
|
TimelineEncryptedHistoryBannerView()
|
||||||
) = ElementPreview {
|
|
||||||
TimelineEncryptedHistoryBannerView(sessionState = sessionState)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.session
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
|
|
||||||
open class SessionStateProvider : PreviewParameterProvider<SessionState> {
|
|
||||||
override val values: Sequence<SessionState>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aSessionState(isSessionVerified = false, isKeyBackupEnabled = false),
|
|
||||||
aSessionState(isSessionVerified = true, isKeyBackupEnabled = false),
|
|
||||||
aSessionState(isSessionVerified = true, isKeyBackupEnabled = true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun aSessionState(
|
|
||||||
isSessionVerified: Boolean = false,
|
|
||||||
isKeyBackupEnabled: Boolean = false,
|
|
||||||
) = SessionState(
|
|
||||||
isSessionVerified = isSessionVerified,
|
|
||||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
|
||||||
)
|
|
||||||
|
|
@ -76,13 +76,11 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
|
||||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
|
@ -745,8 +743,6 @@ class MessagesPresenterTest {
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
appScope = this,
|
appScope = this,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
encryptionService = FakeEncryptionService(),
|
|
||||||
verificationService = FakeSessionVerificationService(),
|
|
||||||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline
|
import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline
|
||||||
|
|
@ -47,13 +46,11 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
|
|
@ -89,7 +86,6 @@ class TimelinePresenterTest {
|
||||||
assertThat(initialState.timelineItems).isEmpty()
|
assertThat(initialState.timelineItems).isEmpty()
|
||||||
val loadedNoTimelineState = awaitItem()
|
val loadedNoTimelineState = awaitItem()
|
||||||
assertThat(loadedNoTimelineState.timelineItems).isEmpty()
|
assertThat(loadedNoTimelineState.timelineItems).isEmpty()
|
||||||
assertThat(loadedNoTimelineState.sessionState).isEqualTo(SessionState(isSessionVerified = false, isKeyBackupEnabled = false))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -512,8 +508,6 @@ class TimelinePresenterTest {
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
appScope = this,
|
appScope = this,
|
||||||
navigator = messagesNavigator,
|
navigator = messagesNavigator,
|
||||||
encryptionService = FakeEncryptionService(),
|
|
||||||
verificationService = FakeSessionVerificationService(),
|
|
||||||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = sendPollResponseAction,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
fun onVerifyClicked()
|
|
||||||
fun onSecureBackupClicked()
|
fun onSecureBackupClicked()
|
||||||
fun onOpenRoomNotificationSettings(roomId: RoomId)
|
fun onOpenRoomNotificationSettings(roomId: RoomId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,10 +115,6 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
plugins<PreferencesEntryPoint.Callback>().forEach { it.onOpenBugReport() }
|
plugins<PreferencesEntryPoint.Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVerifyClicked() {
|
|
||||||
plugins<PreferencesEntryPoint.Callback>().forEach { it.onVerifyClicked() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSecureBackupClicked() {
|
override fun onSecureBackupClicked() {
|
||||||
plugins<PreferencesEntryPoint.Callback>().forEach { it.onSecureBackupClicked() }
|
plugins<PreferencesEntryPoint.Callback>().forEach { it.onSecureBackupClicked() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
fun onVerifyClicked()
|
|
||||||
fun onSecureBackupClicked()
|
fun onSecureBackupClicked()
|
||||||
fun onOpenAnalytics()
|
fun onOpenAnalytics()
|
||||||
fun onOpenAbout()
|
fun onOpenAbout()
|
||||||
|
|
@ -61,10 +60,6 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onVerifyClicked() {
|
|
||||||
plugins<Callback>().forEach { it.onVerifyClicked() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSecureBackupClicked() {
|
private fun onSecureBackupClicked() {
|
||||||
plugins<Callback>().forEach { it.onSecureBackupClicked() }
|
plugins<Callback>().forEach { it.onSecureBackupClicked() }
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +133,6 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
onOpenRageShake = this::onOpenBugReport,
|
onOpenRageShake = this::onOpenBugReport,
|
||||||
onOpenAnalytics = this::onOpenAnalytics,
|
onOpenAnalytics = this::onOpenAnalytics,
|
||||||
onOpenAbout = this::onOpenAbout,
|
onOpenAbout = this::onOpenAbout,
|
||||||
onVerifyClicked = this::onVerifyClicked,
|
|
||||||
onSecureBackupClicked = this::onSecureBackupClicked,
|
onSecureBackupClicked = this::onSecureBackupClicked,
|
||||||
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
|
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
|
||||||
onOpenAdvancedSettings = this::onOpenAdvancedSettings,
|
onOpenAdvancedSettings = this::onOpenAdvancedSettings,
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should display the 'complete verification' option if the current session can be verified
|
// We should display the 'complete verification' option if the current session can be verified
|
||||||
val showCompleteVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(false)
|
val canVerifyUserSession by sessionVerificationService.canVerifySessionFlow.collectAsState(false)
|
||||||
|
|
||||||
val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator()
|
val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator()
|
||||||
|
|
||||||
|
|
@ -102,8 +102,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
myUser = matrixUser.value,
|
myUser = matrixUser.value,
|
||||||
version = versionFormatter.get(),
|
version = versionFormatter.get(),
|
||||||
deviceId = matrixClient.deviceId,
|
deviceId = matrixClient.deviceId,
|
||||||
showCompleteVerification = showCompleteVerification,
|
showSecureBackup = !canVerifyUserSession,
|
||||||
showSecureBackup = !showCompleteVerification,
|
|
||||||
showSecureBackupBadge = showSecureBackupIndicator,
|
showSecureBackupBadge = showSecureBackupIndicator,
|
||||||
accountManagementUrl = accountManagementUrl.value,
|
accountManagementUrl = accountManagementUrl.value,
|
||||||
devicesManagementUrl = devicesManagementUrl.value,
|
devicesManagementUrl = devicesManagementUrl.value,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ data class PreferencesRootState(
|
||||||
val myUser: MatrixUser,
|
val myUser: MatrixUser,
|
||||||
val version: String,
|
val version: String,
|
||||||
val deviceId: String?,
|
val deviceId: String?,
|
||||||
val showCompleteVerification: Boolean,
|
|
||||||
val showSecureBackup: Boolean,
|
val showSecureBackup: Boolean,
|
||||||
val showSecureBackupBadge: Boolean,
|
val showSecureBackupBadge: Boolean,
|
||||||
val accountManagementUrl: String?,
|
val accountManagementUrl: String?,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ fun aPreferencesRootState(
|
||||||
myUser = myUser,
|
myUser = myUser,
|
||||||
version = "Version 1.1 (1)",
|
version = "Version 1.1 (1)",
|
||||||
deviceId = "ILAKNDNASDLK",
|
deviceId = "ILAKNDNASDLK",
|
||||||
showCompleteVerification = true,
|
|
||||||
showSecureBackup = true,
|
showSecureBackup = true,
|
||||||
showSecureBackupBadge = true,
|
showSecureBackupBadge = true,
|
||||||
accountManagementUrl = "aUrl",
|
accountManagementUrl = "aUrl",
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
fun PreferencesRootView(
|
fun PreferencesRootView(
|
||||||
state: PreferencesRootState,
|
state: PreferencesRootState,
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
onVerifyClicked: () -> Unit,
|
|
||||||
onSecureBackupClicked: () -> Unit,
|
onSecureBackupClicked: () -> Unit,
|
||||||
onManageAccountClicked: (url: String) -> Unit,
|
onManageAccountClicked: (url: String) -> Unit,
|
||||||
onOpenAnalytics: () -> Unit,
|
onOpenAnalytics: () -> Unit,
|
||||||
|
|
@ -81,13 +80,6 @@ fun PreferencesRootView(
|
||||||
},
|
},
|
||||||
user = state.myUser,
|
user = state.myUser,
|
||||||
)
|
)
|
||||||
if (state.showCompleteVerification) {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(text = stringResource(CommonStrings.common_verify_device)) },
|
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.CheckCircle())),
|
|
||||||
onClick = onVerifyClicked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (state.showSecureBackup) {
|
if (state.showSecureBackup) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.common_chat_backup)) },
|
headlineContent = { Text(stringResource(id = CommonStrings.common_chat_backup)) },
|
||||||
|
|
@ -95,8 +87,6 @@ fun PreferencesRootView(
|
||||||
trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge },
|
trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge },
|
||||||
onClick = onSecureBackupClicked,
|
onClick = onSecureBackupClicked,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
if (state.showCompleteVerification || state.showSecureBackup) {
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
if (state.accountManagementUrl != null) {
|
if (state.accountManagementUrl != null) {
|
||||||
|
|
@ -232,7 +222,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||||
onOpenDeveloperSettings = {},
|
onOpenDeveloperSettings = {},
|
||||||
onOpenAdvancedSettings = {},
|
onOpenAdvancedSettings = {},
|
||||||
onOpenAbout = {},
|
onOpenAbout = {},
|
||||||
onVerifyClicked = {},
|
|
||||||
onSecureBackupClicked = {},
|
onSecureBackupClicked = {},
|
||||||
onManageAccountClicked = {},
|
onManageAccountClicked = {},
|
||||||
onOpenNotificationSettings = {},
|
onOpenNotificationSettings = {},
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import android.content.Context
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.annotation.ExperimentalCoilApi
|
import coil.annotation.ExperimentalCoilApi
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||||
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
|
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
|
@ -45,7 +45,7 @@ class DefaultClearCacheUseCase @Inject constructor(
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val defaultCacheIndexProvider: DefaultCacheService,
|
private val defaultCacheIndexProvider: DefaultCacheService,
|
||||||
private val okHttpClient: Provider<OkHttpClient>,
|
private val okHttpClient: Provider<OkHttpClient>,
|
||||||
private val ftueState: FtueState,
|
private val ftueService: FtueService,
|
||||||
private val migrationScreenStore: MigrationScreenStore,
|
private val migrationScreenStore: MigrationScreenStore,
|
||||||
) : ClearCacheUseCase {
|
) : ClearCacheUseCase {
|
||||||
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
||||||
|
|
@ -61,7 +61,7 @@ class DefaultClearCacheUseCase @Inject constructor(
|
||||||
// Clear app cache
|
// Clear app cache
|
||||||
context.cacheDir.deleteRecursively()
|
context.cacheDir.deleteRecursively()
|
||||||
// Clear some settings
|
// Clear some settings
|
||||||
ftueState.reset()
|
ftueService.reset()
|
||||||
// Clear migration screen store
|
// Clear migration screen store
|
||||||
migrationScreenStore.reset()
|
migrationScreenStore.reset()
|
||||||
// Ensure the app is restarted
|
// Ensure the app is restarted
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,6 @@ class PreferencesRootPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(initialState.version).isEqualTo("A Version")
|
assertThat(initialState.version).isEqualTo("A Version")
|
||||||
assertThat(loadedState.showCompleteVerification).isTrue()
|
|
||||||
assertThat(loadedState.showSecureBackup).isFalse()
|
assertThat(loadedState.showSecureBackup).isFalse()
|
||||||
assertThat(loadedState.showSecureBackupBadge).isTrue()
|
assertThat(loadedState.showSecureBackupBadge).isTrue()
|
||||||
assertThat(loadedState.accountManagementUrl).isNull()
|
assertThat(loadedState.accountManagementUrl).isNull()
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
||||||
fun onRoomClicked(roomId: RoomId)
|
fun onRoomClicked(roomId: RoomId)
|
||||||
fun onCreateRoomClicked()
|
fun onCreateRoomClicked()
|
||||||
fun onSettingsClicked()
|
fun onSettingsClicked()
|
||||||
fun onSessionVerificationClicked()
|
|
||||||
fun onSessionConfirmRecoveryKeyClicked()
|
fun onSessionConfirmRecoveryKeyClicked()
|
||||||
fun onInvitesClicked()
|
fun onInvitesClicked()
|
||||||
fun onRoomSettingsClicked(roomId: RoomId)
|
fun onRoomSettingsClicked(roomId: RoomId)
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,6 @@ class RoomListNode @AssistedInject constructor(
|
||||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onCreateRoomClicked() }
|
plugins<RoomListEntryPoint.Callback>().forEach { it.onCreateRoomClicked() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSessionVerificationClicked() {
|
|
||||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionVerificationClicked() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSessionConfirmRecoveryKeyClicked() {
|
private fun onSessionConfirmRecoveryKeyClicked() {
|
||||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClicked() }
|
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClicked() }
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +100,6 @@ class RoomListNode @AssistedInject constructor(
|
||||||
onRoomClicked = this::onRoomClicked,
|
onRoomClicked = this::onRoomClicked,
|
||||||
onSettingsClicked = this::onOpenSettings,
|
onSettingsClicked = this::onOpenSettings,
|
||||||
onCreateRoomClicked = this::onCreateRoomClicked,
|
onCreateRoomClicked = this::onCreateRoomClicked,
|
||||||
onVerifyClicked = this::onSessionVerificationClicked,
|
|
||||||
onConfirmRecoveryKeyClicked = this::onSessionConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = this::onSessionConfirmRecoveryKeyClicked,
|
||||||
onInvitesClicked = this::onInvitesClicked,
|
onInvitesClicked = this::onInvitesClicked,
|
||||||
onRoomSettingsClicked = this::onRoomSettingsClicked,
|
onRoomSettingsClicked = this::onRoomSettingsClicked,
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
@ -92,7 +91,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
private val encryptionService: EncryptionService = client.encryptionService()
|
private val encryptionService: EncryptionService = client.encryptionService()
|
||||||
private val sessionVerificationService: SessionVerificationService = client.sessionVerificationService()
|
|
||||||
private val syncService: SyncService = client.syncService()
|
private val syncService: SyncService = client.syncService()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -159,19 +157,12 @@ class RoomListPresenter @Inject constructor(
|
||||||
securityBannerDismissed: Boolean,
|
securityBannerDismissed: Boolean,
|
||||||
): State<SecurityBannerState> {
|
): State<SecurityBannerState> {
|
||||||
val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
|
val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
|
||||||
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
|
|
||||||
val isLastDevice by encryptionService.isLastDevice.collectAsState()
|
|
||||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||||
val syncState by syncService.syncState.collectAsState()
|
val syncState by syncService.syncState.collectAsState()
|
||||||
return remember {
|
return remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
when {
|
when {
|
||||||
currentSecurityBannerDismissed -> SecurityBannerState.None
|
currentSecurityBannerDismissed -> SecurityBannerState.None
|
||||||
canVerifySession -> if (isLastDevice) {
|
|
||||||
SecurityBannerState.RecoveryKeyConfirmation
|
|
||||||
} else {
|
|
||||||
SecurityBannerState.SessionVerification
|
|
||||||
}
|
|
||||||
recoveryState == RecoveryState.INCOMPLETE &&
|
recoveryState == RecoveryState.INCOMPLETE &&
|
||||||
syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation
|
syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation
|
||||||
else -> SecurityBannerState.None
|
else -> SecurityBannerState.None
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ enum class InvitesState {
|
||||||
|
|
||||||
enum class SecurityBannerState {
|
enum class SecurityBannerState {
|
||||||
None,
|
None,
|
||||||
SessionVerification,
|
|
||||||
RecoveryKeyConfirmation,
|
RecoveryKeyConfirmation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.NewInvites)),
|
aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.NewInvites)),
|
||||||
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
||||||
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
|
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
|
||||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification)),
|
|
||||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
||||||
aRoomListState(contentState = anEmptyContentState()),
|
aRoomListState(contentState = anEmptyContentState()),
|
||||||
aRoomListState(contentState = aSkeletonContentState()),
|
aRoomListState(contentState = aSkeletonContentState()),
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ fun RoomListView(
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
onRoomClicked: (RoomId) -> Unit,
|
onRoomClicked: (RoomId) -> Unit,
|
||||||
onSettingsClicked: () -> Unit,
|
onSettingsClicked: () -> Unit,
|
||||||
onVerifyClicked: () -> Unit,
|
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onCreateRoomClicked: () -> Unit,
|
onCreateRoomClicked: () -> Unit,
|
||||||
onInvitesClicked: () -> Unit,
|
onInvitesClicked: () -> Unit,
|
||||||
|
|
@ -86,7 +85,6 @@ fun RoomListView(
|
||||||
RoomListScaffold(
|
RoomListScaffold(
|
||||||
modifier = Modifier.padding(top = topPadding),
|
modifier = Modifier.padding(top = topPadding),
|
||||||
state = state,
|
state = state,
|
||||||
onVerifyClicked = onVerifyClicked,
|
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||||
|
|
@ -115,7 +113,6 @@ fun RoomListView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomListScaffold(
|
private fun RoomListScaffold(
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
onVerifyClicked: () -> Unit,
|
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onRoomClicked: (RoomId) -> Unit,
|
onRoomClicked: (RoomId) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
|
@ -154,7 +151,6 @@ private fun RoomListScaffold(
|
||||||
contentState = state.contentState,
|
contentState = state.contentState,
|
||||||
filtersState = state.filtersState,
|
filtersState = state.filtersState,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
onVerifyClicked = onVerifyClicked,
|
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onRoomClicked = ::onRoomClicked,
|
onRoomClicked = ::onRoomClicked,
|
||||||
onRoomLongClicked = onRoomLongClicked,
|
onRoomLongClicked = onRoomLongClicked,
|
||||||
|
|
@ -193,7 +189,6 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClicked = {},
|
onRoomClicked = {},
|
||||||
onSettingsClicked = {},
|
onSettingsClicked = {},
|
||||||
onVerifyClicked = {},
|
|
||||||
onConfirmRecoveryKeyClicked = {},
|
onConfirmRecoveryKeyClicked = {},
|
||||||
onCreateRoomClicked = {},
|
onCreateRoomClicked = {},
|
||||||
onInvitesClicked = {},
|
onInvitesClicked = {},
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl.components
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import io.element.android.features.roomlist.impl.R
|
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun RequestVerificationHeader(
|
|
||||||
onVerifyClicked: () -> Unit,
|
|
||||||
onDismissClicked: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
DialogLikeBannerMolecule(
|
|
||||||
modifier = modifier,
|
|
||||||
title = stringResource(R.string.session_verification_banner_title),
|
|
||||||
content = stringResource(R.string.session_verification_banner_message),
|
|
||||||
onSubmitClicked = onVerifyClicked,
|
|
||||||
onDismissClicked = onDismissClicked,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun RequestVerificationHeaderPreview() = ElementPreview {
|
|
||||||
RequestVerificationHeader(
|
|
||||||
onVerifyClicked = {},
|
|
||||||
onDismissClicked = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -73,7 +73,6 @@ fun RoomListContentView(
|
||||||
contentState: RoomListContentState,
|
contentState: RoomListContentState,
|
||||||
filtersState: RoomListFiltersState,
|
filtersState: RoomListFiltersState,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onVerifyClicked: () -> Unit,
|
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
|
@ -103,7 +102,6 @@ fun RoomListContentView(
|
||||||
state = contentState,
|
state = contentState,
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onVerifyClicked = onVerifyClicked,
|
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = onRoomLongClicked,
|
onRoomLongClicked = onRoomLongClicked,
|
||||||
|
|
@ -161,7 +159,6 @@ private fun RoomsView(
|
||||||
state: RoomListContentState.Rooms,
|
state: RoomListContentState.Rooms,
|
||||||
filtersState: RoomListFiltersState,
|
filtersState: RoomListFiltersState,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onVerifyClicked: () -> Unit,
|
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
|
@ -177,7 +174,6 @@ private fun RoomsView(
|
||||||
RoomsViewList(
|
RoomsViewList(
|
||||||
state = state,
|
state = state,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onVerifyClicked = onVerifyClicked,
|
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = onRoomLongClicked,
|
onRoomLongClicked = onRoomLongClicked,
|
||||||
|
|
@ -191,7 +187,6 @@ private fun RoomsView(
|
||||||
private fun RoomsViewList(
|
private fun RoomsViewList(
|
||||||
state: RoomListContentState.Rooms,
|
state: RoomListContentState.Rooms,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onVerifyClicked: () -> Unit,
|
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||||
|
|
@ -222,14 +217,6 @@ private fun RoomsViewList(
|
||||||
contentPadding = PaddingValues(bottom = 80.dp)
|
contentPadding = PaddingValues(bottom = 80.dp)
|
||||||
) {
|
) {
|
||||||
when (state.securityBannerState) {
|
when (state.securityBannerState) {
|
||||||
SecurityBannerState.SessionVerification -> {
|
|
||||||
item {
|
|
||||||
RequestVerificationHeader(
|
|
||||||
onVerifyClicked = onVerifyClicked,
|
|
||||||
onDismissClicked = { eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SecurityBannerState.RecoveryKeyConfirmation -> {
|
SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||||
item {
|
item {
|
||||||
ConfirmRecoveryKeyBanner(
|
ConfirmRecoveryKeyBanner(
|
||||||
|
|
@ -316,10 +303,10 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
|
||||||
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
|
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
|
||||||
),
|
),
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
onVerifyClicked = { },
|
onConfirmRecoveryKeyClicked = {},
|
||||||
onConfirmRecoveryKeyClicked = { },
|
|
||||||
onRoomClicked = {},
|
onRoomClicked = {},
|
||||||
onRoomLongClicked = {},
|
onRoomLongClicked = {},
|
||||||
onCreateRoomClicked = { },
|
onCreateRoomClicked = {},
|
||||||
onInvitesClicked = { })
|
onInvitesClicked = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -239,52 +239,28 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - handle RecoveryKeyConfirmation last session`() = runTest {
|
|
||||||
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
|
||||||
val roomListService = FakeRoomListService().apply {
|
|
||||||
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
|
||||||
}
|
|
||||||
val presenter = createRoomListPresenter(
|
|
||||||
coroutineScope = scope,
|
|
||||||
client = FakeMatrixClient(
|
|
||||||
encryptionService = FakeEncryptionService().apply {
|
|
||||||
emitIsLastDevice(true)
|
|
||||||
},
|
|
||||||
roomListService = roomListService
|
|
||||||
),
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val eventSink = consumeItemsUntilPredicate {
|
|
||||||
it.contentState is RoomListContentState.Rooms
|
|
||||||
}.last().eventSink
|
|
||||||
// For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation
|
|
||||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
|
||||||
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
|
||||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
||||||
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomListService = FakeRoomListService().apply {
|
||||||
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||||
}
|
}
|
||||||
|
val encryptionService = FakeEncryptionService().apply {
|
||||||
|
emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
|
}
|
||||||
|
val syncService = FakeSyncService(initialState = SyncState.Running)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = FakeMatrixClient(roomListService = roomListService),
|
client = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService, syncService = syncService),
|
||||||
coroutineScope = scope,
|
coroutineScope = scope,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val eventSink = consumeItemsUntilPredicate {
|
val eventWithContentAsRooms = consumeItemsUntilPredicate {
|
||||||
it.contentState is RoomListContentState.Rooms
|
it.contentState is RoomListContentState.Rooms
|
||||||
}.last().eventSink
|
}.last()
|
||||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification)
|
val eventSink = eventWithContentAsRooms.eventSink
|
||||||
|
assertThat(eventWithContentAsRooms.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
||||||
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
||||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
|
@ -342,10 +318,10 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
consumeItemsUntilPredicate {
|
val firstItem = consumeItemsUntilPredicate {
|
||||||
it.contentState is RoomListContentState.Rooms
|
it.contentState is RoomListContentState.Rooms
|
||||||
}
|
}.last()
|
||||||
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
|
assertThat(firstItem.contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
|
||||||
|
|
||||||
inviteStateFlow.value = InvitesState.SeenInvites
|
inviteStateFlow.value = InvitesState.SeenInvites
|
||||||
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.SeenInvites)
|
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.SeenInvites)
|
||||||
|
|
|
||||||
|
|
@ -43,35 +43,6 @@ import org.junit.runner.RunWith
|
||||||
class RoomListViewTest {
|
class RoomListViewTest {
|
||||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking on close verification banner emits the expected Event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
|
||||||
rule.setRoomListView(
|
|
||||||
state = aRoomListState(
|
|
||||||
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification),
|
|
||||||
eventSink = eventsRecorder,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val close = rule.activity.getString(CommonStrings.action_close)
|
|
||||||
rule.onNodeWithContentDescription(close).performClick()
|
|
||||||
eventsRecorder.assertSingle(RoomListEvents.DismissRequestVerificationPrompt)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking on continue verification banner invokes the expected callback`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>(expectEvents = false)
|
|
||||||
ensureCalledOnce { callback ->
|
|
||||||
rule.setRoomListView(
|
|
||||||
state = aRoomListState(
|
|
||||||
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification),
|
|
||||||
eventSink = eventsRecorder,
|
|
||||||
),
|
|
||||||
onVerifyClicked = callback,
|
|
||||||
)
|
|
||||||
rule.clickOn(CommonStrings.action_continue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on close recovery key banner emits the expected Event`() {
|
fun `clicking on close recovery key banner emits the expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||||
|
|
@ -185,7 +156,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
onRoomClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
onRoomClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onSettingsClicked: () -> Unit = EnsureNeverCalled(),
|
onSettingsClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
onVerifyClicked: () -> Unit = EnsureNeverCalled(),
|
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(),
|
onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
onCreateRoomClicked: () -> Unit = EnsureNeverCalled(),
|
onCreateRoomClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
|
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
|
|
@ -198,7 +168,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onSettingsClicked = onSettingsClicked,
|
onSettingsClicked = onSettingsClicked,
|
||||||
onVerifyClicked = onVerifyClicked,
|
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onCreateRoomClicked = onCreateRoomClicked,
|
onCreateRoomClicked = onCreateRoomClicked,
|
||||||
onInvitesClicked = onInvitesClicked,
|
onInvitesClicked = onInvitesClicked,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.securebackup.api
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
@ -36,8 +37,13 @@ interface SecureBackupEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onDone()
|
||||||
|
}
|
||||||
|
|
||||||
interface NodeBuilder {
|
interface NodeBuilder {
|
||||||
fun params(params: Params): NodeBuilder
|
fun params(params: Params): NodeBuilder
|
||||||
|
fun callback(callback: Callback): NodeBuilder
|
||||||
fun build(): Node
|
fun build(): Node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,11 @@ class DefaultSecureBackupEntryPoint @Inject constructor() : SecureBackupEntryPoi
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun callback(callback: SecureBackupEntryPoint.Callback): SecureBackupEntryPoint.NodeBuilder {
|
||||||
|
plugins += callback
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
override fun build(): Node {
|
override fun build(): Node {
|
||||||
return parentNode.createNode<SecureBackupFlowNode>(buildContext, plugins)
|
return parentNode.createNode<SecureBackupFlowNode>(buildContext, plugins)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
@ -74,6 +76,8 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||||
data object EnterRecoveryKey : NavTarget
|
data object EnterRecoveryKey : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val callback = plugins<SecureBackupEntryPoint.Callback>().firstOrNull()
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Root -> {
|
NavTarget.Root -> {
|
||||||
|
|
@ -119,7 +123,16 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||||
createNode<SecureBackupEnableNode>(buildContext)
|
createNode<SecureBackupEnableNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.EnterRecoveryKey -> {
|
NavTarget.EnterRecoveryKey -> {
|
||||||
createNode<SecureBackupEnterRecoveryKeyNode>(buildContext)
|
val callback = object : SecureBackupEnterRecoveryKeyNode.Callback {
|
||||||
|
override fun onEnterRecoveryKeySuccess() {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onDone()
|
||||||
|
} else {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<SecureBackupEnterRecoveryKeyNode>(buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,48 +17,36 @@
|
||||||
package io.element.android.features.securebackup.impl.enter
|
package io.element.android.features.securebackup.impl.enter
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.securebackup.impl.R
|
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor(
|
class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: SecureBackupEnterRecoveryKeyPresenter,
|
private val presenter: SecureBackupEnterRecoveryKeyPresenter,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onEnterRecoveryKeySuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callback = plugins<Callback>().first()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
SecureBackupEnterRecoveryKeyView(
|
SecureBackupEnterRecoveryKeyView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onDone = {
|
onDone = callback::onEnterRecoveryKeySuccess,
|
||||||
coroutineScope.postSuccessSnackbar()
|
|
||||||
navigateUp()
|
|
||||||
},
|
|
||||||
onBackClicked = ::navigateUp,
|
onBackClicked = ::navigateUp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.postSuccessSnackbar() = launch {
|
|
||||||
snackbarDispatcher.post(
|
|
||||||
SnackbarMessage(
|
|
||||||
messageResId = R.string.screen_recovery_key_confirm_success
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ class VerifySelfSessionNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onEnterRecoveryKey = ::onEnterRecoveryKey,
|
onEnterRecoveryKey = ::onEnterRecoveryKey,
|
||||||
goBack = ::onDone,
|
onFinished = ::onDone,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,10 +68,10 @@ class VerifySelfSessionPresenter @Inject constructor(
|
||||||
when (event) {
|
when (event) {
|
||||||
VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification)
|
VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification)
|
||||||
VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification)
|
VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification)
|
||||||
VerifySelfSessionViewEvents.Restart -> stateAndDispatch.dispatchAction(StateMachineEvent.Restart)
|
|
||||||
VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge)
|
VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge)
|
||||||
VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
|
VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
|
||||||
VerifySelfSessionViewEvents.CancelAndClose -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
|
VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
|
||||||
|
VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return VerifySelfSessionState(
|
return VerifySelfSessionState(
|
||||||
|
|
@ -118,7 +118,7 @@ class VerifySelfSessionPresenter @Inject constructor(
|
||||||
private fun CoroutineScope.observeVerificationService() {
|
private fun CoroutineScope.observeVerificationService() {
|
||||||
sessionVerificationService.verificationFlowState.onEach { verificationAttemptState ->
|
sessionVerificationService.verificationFlowState.onEach { verificationAttemptState ->
|
||||||
when (verificationAttemptState) {
|
when (verificationAttemptState) {
|
||||||
VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Restart)
|
VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Reset)
|
||||||
VerificationFlowState.AcceptedVerificationRequest -> {
|
VerificationFlowState.AcceptedVerificationRequest -> {
|
||||||
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest)
|
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,24 +20,35 @@
|
||||||
package io.element.android.features.verifysession.impl
|
package io.element.android.features.verifysession.impl
|
||||||
|
|
||||||
import com.freeletics.flowredux.dsl.FlowReduxStateMachine
|
import com.freeletics.flowredux.dsl.FlowReduxStateMachine
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
|
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.verification.SessionVerificationData
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.timeout
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import com.freeletics.flowredux.dsl.State as MachineState
|
import com.freeletics.flowredux.dsl.State as MachineState
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
class VerifySelfSessionStateMachine @Inject constructor(
|
class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
|
private val encryptionService: EncryptionService,
|
||||||
) : FlowReduxStateMachine<VerifySelfSessionStateMachine.State, VerifySelfSessionStateMachine.Event>(
|
) : FlowReduxStateMachine<VerifySelfSessionStateMachine.State, VerifySelfSessionStateMachine.Event>(
|
||||||
initialState = State.Initial
|
initialState = State.Initial
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
spec {
|
spec {
|
||||||
inState<State.Initial> {
|
inState<State.Initial> {
|
||||||
on { _: Event.RequestVerification, state: MachineState<State.Initial> ->
|
on { _: Event.RequestVerification, state ->
|
||||||
state.override { State.RequestingVerification }
|
state.override { State.RequestingVerification }
|
||||||
}
|
}
|
||||||
on { _: Event.StartSasVerification, state: MachineState<State.Initial> ->
|
on { _: Event.StartSasVerification, state ->
|
||||||
state.override { State.StartingSasVerification }
|
state.override { State.StartingSasVerification }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -45,12 +56,9 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
onEnterEffect {
|
onEnterEffect {
|
||||||
sessionVerificationService.requestVerification()
|
sessionVerificationService.requestVerification()
|
||||||
}
|
}
|
||||||
on { _: Event.DidAcceptVerificationRequest, state: MachineState<State.RequestingVerification> ->
|
on { _: Event.DidAcceptVerificationRequest, state ->
|
||||||
state.override { State.VerificationRequestAccepted }
|
state.override { State.VerificationRequestAccepted }
|
||||||
}
|
}
|
||||||
on { _: Event.DidFail, state: MachineState<State.RequestingVerification> ->
|
|
||||||
state.override { State.Initial }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
inState<State.StartingSasVerification> {
|
inState<State.StartingSasVerification> {
|
||||||
onEnterEffect {
|
onEnterEffect {
|
||||||
|
|
@ -58,25 +66,28 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inState<State.VerificationRequestAccepted> {
|
inState<State.VerificationRequestAccepted> {
|
||||||
on { _: Event.StartSasVerification, state: MachineState<State.VerificationRequestAccepted> ->
|
on { _: Event.StartSasVerification, state ->
|
||||||
state.override { State.StartingSasVerification }
|
state.override { State.StartingSasVerification }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inState<State.Canceled> {
|
inState<State.Canceled> {
|
||||||
on { _: Event.Restart, state: MachineState<State.Canceled> ->
|
on { _: Event.RequestVerification, state ->
|
||||||
state.override { State.RequestingVerification }
|
state.override { State.RequestingVerification }
|
||||||
}
|
}
|
||||||
|
on { _: Event.Reset, state ->
|
||||||
|
state.override { State.Initial }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inState<State.SasVerificationStarted> {
|
inState<State.SasVerificationStarted> {
|
||||||
on { event: Event.DidReceiveChallenge, state: MachineState<State.SasVerificationStarted> ->
|
on { event: Event.DidReceiveChallenge, state ->
|
||||||
state.override { State.Verifying.ChallengeReceived(event.data) }
|
state.override { State.Verifying.ChallengeReceived(event.data) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inState<State.Verifying.ChallengeReceived> {
|
inState<State.Verifying.ChallengeReceived> {
|
||||||
on { _: Event.AcceptChallenge, state: MachineState<State.Verifying.ChallengeReceived> ->
|
on { _: Event.AcceptChallenge, state ->
|
||||||
state.override { State.Verifying.Replying(state.snapshot.data, accept = true) }
|
state.override { State.Verifying.Replying(state.snapshot.data, accept = true) }
|
||||||
}
|
}
|
||||||
on { _: Event.DeclineChallenge, state: MachineState<State.Verifying.ChallengeReceived> ->
|
on { _: Event.DeclineChallenge, state ->
|
||||||
state.override { State.Verifying.Replying(state.snapshot.data, accept = false) }
|
state.override { State.Verifying.Replying(state.snapshot.data, accept = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,11 +99,21 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
sessionVerificationService.declineVerification()
|
sessionVerificationService.declineVerification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
on { _: Event.DidAcceptChallenge, state: MachineState<State.Verifying.Replying> ->
|
on { _: Event.DidAcceptChallenge, state ->
|
||||||
|
// If a key backup exists, wait until it's restored or a timeout happens
|
||||||
|
val hasBackup = encryptionService.doesBackupExistOnServer().getOrNull().orFalse()
|
||||||
|
if (hasBackup) {
|
||||||
|
tryOrNull {
|
||||||
|
encryptionService.recoveryStateStateFlow.filter { it == RecoveryState.ENABLED }
|
||||||
|
.timeout(10.seconds)
|
||||||
|
.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
state.override { State.Completed }
|
state.override { State.Completed }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inState<State.Canceling> {
|
inState<State.Canceling> {
|
||||||
|
// TODO The 'Canceling' -> 'Canceled' transitions doesn't seem to work anymore, check if something changed in the Rust SDK
|
||||||
onEnterEffect {
|
onEnterEffect {
|
||||||
sessionVerificationService.cancelVerification()
|
sessionVerificationService.cancelVerification()
|
||||||
}
|
}
|
||||||
|
|
@ -102,21 +123,24 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
state.override { State.SasVerificationStarted }
|
state.override { State.SasVerificationStarted }
|
||||||
}
|
}
|
||||||
on { _: Event.Cancel, state: MachineState<State> ->
|
on { _: Event.Cancel, state: MachineState<State> ->
|
||||||
if (state.snapshot in sequenceOf(
|
when (state.snapshot) {
|
||||||
State.Initial,
|
State.Initial, State.Completed, State.Canceled -> state.noChange()
|
||||||
State.Completed,
|
// For some reason `cancelVerification` is not calling its delegate `didCancel` method so we don't pass from
|
||||||
State.Canceled
|
// `Canceling` state to `Canceled` automatically anymore
|
||||||
)) {
|
else -> {
|
||||||
state.noChange()
|
sessionVerificationService.cancelVerification()
|
||||||
} else {
|
state.override { State.Canceled }
|
||||||
state.override { State.Canceling }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
on { _: Event.DidCancel, state: MachineState<State> ->
|
on { _: Event.DidCancel, state: MachineState<State> ->
|
||||||
state.override { State.Canceled }
|
state.override { State.Canceled }
|
||||||
}
|
}
|
||||||
on { _: Event.DidFail, state: MachineState<State> ->
|
on { _: Event.DidFail, state: MachineState<State> ->
|
||||||
state.override { State.Canceled }
|
when (state.snapshot) {
|
||||||
|
is State.RequestingVerification -> state.override { State.Initial }
|
||||||
|
else -> state.override { State.Canceled }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +214,7 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
/** Request failed. */
|
/** Request failed. */
|
||||||
data object DidFail : Event
|
data object DidFail : Event
|
||||||
|
|
||||||
/** Restart the verification flow. */
|
/** Reset the verification flow to the initial state. */
|
||||||
data object Restart : Event
|
data object Reset : Event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,6 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
|
@ -42,15 +39,16 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.verifysession.impl.emoji.toEmojiResource
|
import io.element.android.features.verifysession.impl.emoji.toEmojiResource
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
|
import io.element.android.libraries.designsystem.components.PageTitle
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
|
||||||
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.components.TextButton
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||||
|
|
@ -62,36 +60,37 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver
|
||||||
fun VerifySelfSessionView(
|
fun VerifySelfSessionView(
|
||||||
state: VerifySelfSessionState,
|
state: VerifySelfSessionState,
|
||||||
onEnterRecoveryKey: () -> Unit,
|
onEnterRecoveryKey: () -> Unit,
|
||||||
goBack: () -> Unit,
|
onFinished: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun goBackAndCancelIfNeeded() {
|
fun resetFlow() {
|
||||||
state.eventSink(VerifySelfSessionViewEvents.CancelAndClose)
|
state.eventSink(VerifySelfSessionViewEvents.Reset)
|
||||||
goBack()
|
|
||||||
}
|
|
||||||
if (state.verificationFlowStep is FlowStep.Completed) {
|
|
||||||
goBack()
|
|
||||||
}
|
}
|
||||||
BackHandler {
|
BackHandler {
|
||||||
goBackAndCancelIfNeeded()
|
when (state.verificationFlowStep) {
|
||||||
|
is FlowStep.Canceled -> resetFlow()
|
||||||
|
is FlowStep.AwaitingOtherDeviceResponse, FlowStep.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel)
|
||||||
|
is FlowStep.Verifying -> {
|
||||||
|
if (!state.verificationFlowStep.state.isLoading()) {
|
||||||
|
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val verificationFlowStep = state.verificationFlowStep
|
val verificationFlowStep = state.verificationFlowStep
|
||||||
val buttonsVisible by remember(verificationFlowStep) {
|
|
||||||
derivedStateOf { verificationFlowStep != FlowStep.AwaitingOtherDeviceResponse && verificationFlowStep != FlowStep.Completed }
|
|
||||||
}
|
|
||||||
HeaderFooterPage(
|
HeaderFooterPage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
header = {
|
header = {
|
||||||
HeaderContent(verificationFlowStep = verificationFlowStep)
|
HeaderContent(verificationFlowStep = verificationFlowStep)
|
||||||
},
|
},
|
||||||
footer = {
|
footer = {
|
||||||
if (buttonsVisible) {
|
BottomMenu(
|
||||||
BottomMenu(
|
screenState = state,
|
||||||
screenState = state,
|
goBack = ::resetFlow,
|
||||||
goBack = ::goBackAndCancelIfNeeded,
|
onEnterRecoveryKey = onEnterRecoveryKey,
|
||||||
onEnterRecoveryKey = onEnterRecoveryKey
|
onFinished = onFinished,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Content(flowState = verificationFlowStep)
|
Content(flowState = verificationFlowStep)
|
||||||
|
|
@ -100,40 +99,38 @@ fun VerifySelfSessionView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HeaderContent(verificationFlowStep: FlowStep) {
|
private fun HeaderContent(verificationFlowStep: FlowStep) {
|
||||||
val iconResourceId = when (verificationFlowStep) {
|
val iconStyle = when (verificationFlowStep) {
|
||||||
is FlowStep.Initial -> R.drawable.ic_verification_devices
|
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.LockSolid())
|
||||||
FlowStep.Canceled -> R.drawable.ic_verification_warning
|
FlowStep.Canceled -> BigIcon.Style.AlertSolid
|
||||||
FlowStep.AwaitingOtherDeviceResponse -> R.drawable.ic_verification_waiting
|
FlowStep.Ready, is FlowStep.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
|
||||||
FlowStep.Ready, is FlowStep.Verifying, FlowStep.Completed -> R.drawable.ic_verification_emoji
|
FlowStep.Completed -> BigIcon.Style.SuccessSolid
|
||||||
}
|
}
|
||||||
val titleTextId = when (verificationFlowStep) {
|
val titleTextId = when (verificationFlowStep) {
|
||||||
is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title
|
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title
|
||||||
FlowStep.Canceled -> CommonStrings.common_verification_cancelled
|
FlowStep.Canceled -> CommonStrings.common_verification_cancelled
|
||||||
FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_title
|
FlowStep.Ready -> R.string.screen_session_verification_compare_emojis_title
|
||||||
FlowStep.Ready,
|
FlowStep.Completed -> R.string.screen_identity_confirmed_title
|
||||||
FlowStep.Completed -> R.string.screen_session_verification_compare_emojis_title
|
|
||||||
is FlowStep.Verifying -> when (verificationFlowStep.data) {
|
is FlowStep.Verifying -> when (verificationFlowStep.data) {
|
||||||
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
|
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
|
||||||
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title
|
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val subtitleTextId = when (verificationFlowStep) {
|
val subtitleTextId = when (verificationFlowStep) {
|
||||||
is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle
|
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle
|
||||||
FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle
|
FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle
|
||||||
FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_subtitle
|
|
||||||
FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle
|
FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle
|
||||||
FlowStep.Completed -> R.string.screen_session_verification_compare_emojis_subtitle
|
FlowStep.Completed -> R.string.screen_identity_confirmation_subtitle
|
||||||
is FlowStep.Verifying -> when (verificationFlowStep.data) {
|
is FlowStep.Verifying -> when (verificationFlowStep.data) {
|
||||||
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
|
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
|
||||||
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle
|
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconTitleSubtitleMolecule(
|
PageTitle(
|
||||||
modifier = Modifier.padding(top = 60.dp),
|
modifier = Modifier.padding(top = 60.dp),
|
||||||
iconResourceId = iconResourceId,
|
iconStyle = iconStyle,
|
||||||
title = stringResource(id = titleTextId),
|
title = stringResource(id = titleTextId),
|
||||||
subTitle = stringResource(id = subtitleTextId)
|
subtitle = stringResource(id = subtitleTextId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,20 +138,12 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
|
||||||
private fun Content(flowState: FlowStep) {
|
private fun Content(flowState: FlowStep) {
|
||||||
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
|
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
|
||||||
when (flowState) {
|
when (flowState) {
|
||||||
is FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
|
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
|
||||||
FlowStep.AwaitingOtherDeviceResponse -> ContentWaiting()
|
|
||||||
is FlowStep.Verifying -> ContentVerifying(flowState)
|
is FlowStep.Verifying -> ContentVerifying(flowState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ContentWaiting() {
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) {
|
private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) {
|
||||||
when (verificationFlowStep.data) {
|
when (verificationFlowStep.data) {
|
||||||
|
|
@ -212,76 +201,102 @@ private fun BottomMenu(
|
||||||
screenState: VerifySelfSessionState,
|
screenState: VerifySelfSessionState,
|
||||||
onEnterRecoveryKey: () -> Unit,
|
onEnterRecoveryKey: () -> Unit,
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
|
onFinished: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val verificationViewState = screenState.verificationFlowStep
|
val verificationViewState = screenState.verificationFlowStep
|
||||||
val eventSink = screenState.eventSink
|
val eventSink = screenState.eventSink
|
||||||
|
|
||||||
val isVerifying = (verificationViewState as? FlowStep.Verifying)?.state is AsyncData.Loading<Unit>
|
val isVerifying = (verificationViewState as? FlowStep.Verifying)?.state is AsyncData.Loading<Unit>
|
||||||
val positiveButtonTitle = when (verificationViewState) {
|
|
||||||
is FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial
|
when (verificationViewState) {
|
||||||
FlowStep.Canceled -> R.string.screen_session_verification_positive_button_canceled
|
is FlowStep.Initial -> {
|
||||||
|
BottomMenu(
|
||||||
|
positiveButtonTitle = stringResource(R.string.screen_identity_use_another_device),
|
||||||
|
onPositiveButtonClicked = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
||||||
|
negativeButtonTitle = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
||||||
|
onNegativeButtonClicked = onEnterRecoveryKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is FlowStep.Canceled -> {
|
||||||
|
BottomMenu(
|
||||||
|
positiveButtonTitle = stringResource(R.string.screen_session_verification_positive_button_canceled),
|
||||||
|
onPositiveButtonClicked = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
||||||
|
negativeButtonTitle = stringResource(CommonStrings.action_cancel),
|
||||||
|
onNegativeButtonClicked = goBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is FlowStep.Ready -> {
|
||||||
|
BottomMenu(
|
||||||
|
positiveButtonTitle = stringResource(CommonStrings.action_start),
|
||||||
|
onPositiveButtonClicked = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) },
|
||||||
|
negativeButtonTitle = stringResource(CommonStrings.action_cancel),
|
||||||
|
onNegativeButtonClicked = goBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is FlowStep.AwaitingOtherDeviceResponse -> {
|
||||||
|
BottomMenu(
|
||||||
|
positiveButtonTitle = stringResource(R.string.screen_identity_waiting_on_other_device),
|
||||||
|
onPositiveButtonClicked = {},
|
||||||
|
isLoading = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
is FlowStep.Verifying -> {
|
is FlowStep.Verifying -> {
|
||||||
if (isVerifying) {
|
val positiveButtonTitle = if (isVerifying) {
|
||||||
R.string.screen_session_verification_positive_button_verifying_ongoing
|
stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing)
|
||||||
} else {
|
} else {
|
||||||
R.string.screen_session_verification_they_match
|
stringResource(R.string.screen_session_verification_they_match)
|
||||||
}
|
}
|
||||||
|
BottomMenu(
|
||||||
|
positiveButtonTitle = positiveButtonTitle,
|
||||||
|
onPositiveButtonClicked = {
|
||||||
|
if (!isVerifying) {
|
||||||
|
eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
negativeButtonTitle = stringResource(R.string.screen_session_verification_they_dont_match),
|
||||||
|
onNegativeButtonClicked = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) },
|
||||||
|
isLoading = isVerifying,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
FlowStep.Ready -> CommonStrings.action_start
|
is FlowStep.Completed -> {
|
||||||
else -> null
|
BottomMenu(
|
||||||
}
|
positiveButtonTitle = stringResource(CommonStrings.action_continue),
|
||||||
val negativeButtonTitle = when (verificationViewState) {
|
onPositiveButtonClicked = onFinished,
|
||||||
is FlowStep.Initial -> CommonStrings.action_cancel
|
)
|
||||||
FlowStep.Canceled -> CommonStrings.action_cancel
|
|
||||||
is FlowStep.Verifying -> R.string.screen_session_verification_they_dont_match
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
val negativeButtonEnabled = !isVerifying
|
|
||||||
|
|
||||||
val positiveButtonEvent = when (verificationViewState) {
|
|
||||||
is FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification
|
|
||||||
FlowStep.Ready -> VerifySelfSessionViewEvents.StartSasVerification
|
|
||||||
is FlowStep.Verifying -> if (!isVerifying) VerifySelfSessionViewEvents.ConfirmVerification else null
|
|
||||||
FlowStep.Canceled -> VerifySelfSessionViewEvents.Restart
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val negativeButtonCallback: () -> Unit = when (verificationViewState) {
|
|
||||||
is FlowStep.Verifying -> {
|
|
||||||
{ eventSink(VerifySelfSessionViewEvents.DeclineVerification) }
|
|
||||||
}
|
}
|
||||||
else -> goBack
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BottomMenu(
|
||||||
|
positiveButtonTitle: String?,
|
||||||
|
onPositiveButtonClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
negativeButtonTitle: String? = null,
|
||||||
|
negativeButtonEnabled: Boolean = negativeButtonTitle != null,
|
||||||
|
onNegativeButtonClicked: () -> Unit = {},
|
||||||
|
isLoading: Boolean = false,
|
||||||
|
) {
|
||||||
ButtonColumnMolecule(
|
ButtonColumnMolecule(
|
||||||
modifier = Modifier.padding(bottom = 20.dp)
|
modifier = modifier.padding(bottom = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (positiveButtonTitle != null) {
|
if (positiveButtonTitle != null) {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(positiveButtonTitle),
|
text = positiveButtonTitle,
|
||||||
showProgress = isVerifying,
|
showProgress = isLoading,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = { positiveButtonEvent?.let { eventSink(it) } }
|
onClick = onPositiveButtonClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (negativeButtonTitle != null) {
|
if (negativeButtonTitle != null) {
|
||||||
TextButton(
|
TextButton(
|
||||||
text = stringResource(negativeButtonTitle),
|
text = negativeButtonTitle,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = negativeButtonCallback,
|
onClick = onNegativeButtonClicked,
|
||||||
enabled = negativeButtonEnabled,
|
enabled = negativeButtonEnabled,
|
||||||
)
|
)
|
||||||
}
|
} else {
|
||||||
if (verificationViewState is FlowStep.Initial && verificationViewState.canEnterRecoveryKey) {
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
Text(
|
|
||||||
text = stringResource(id = CommonStrings.common_or),
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
)
|
|
||||||
TextButton(
|
|
||||||
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
onClick = onEnterRecoveryKey,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -292,6 +307,6 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta
|
||||||
VerifySelfSessionView(
|
VerifySelfSessionView(
|
||||||
state = state,
|
state = state,
|
||||||
onEnterRecoveryKey = {},
|
onEnterRecoveryKey = {},
|
||||||
goBack = {},
|
onFinished = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ package io.element.android.features.verifysession.impl
|
||||||
sealed interface VerifySelfSessionViewEvents {
|
sealed interface VerifySelfSessionViewEvents {
|
||||||
data object RequestVerification : VerifySelfSessionViewEvents
|
data object RequestVerification : VerifySelfSessionViewEvents
|
||||||
data object StartSasVerification : VerifySelfSessionViewEvents
|
data object StartSasVerification : VerifySelfSessionViewEvents
|
||||||
data object Restart : VerifySelfSessionViewEvents
|
|
||||||
data object ConfirmVerification : VerifySelfSessionViewEvents
|
data object ConfirmVerification : VerifySelfSessionViewEvents
|
||||||
data object DeclineVerification : VerifySelfSessionViewEvents
|
data object DeclineVerification : VerifySelfSessionViewEvents
|
||||||
data object CancelAndClose : VerifySelfSessionViewEvents
|
data object Cancel : VerifySelfSessionViewEvents
|
||||||
|
data object Reset : VerifySelfSessionViewEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="48dp"
|
|
||||||
android:height="48dp"
|
|
||||||
android:viewportWidth="48"
|
|
||||||
android:viewportHeight="48">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M8.3,35.5V11Q8.3,9.8 9.2,8.9Q10.1,8 11.3,8H40.8Q41.45,8 41.875,8.425Q42.3,8.85 42.3,9.5Q42.3,10.15 41.875,10.575Q41.45,11 40.8,11H11.3Q11.3,11 11.3,11Q11.3,11 11.3,11V35.5H20.75Q21.7,35.5 22.35,36.15Q23,36.8 23,37.75Q23,38.7 22.35,39.35Q21.7,40 20.75,40H6.25Q5.3,40 4.65,39.35Q4,38.7 4,37.75Q4,36.8 4.65,36.15Q5.3,35.5 6.25,35.5ZM27.95,40Q27.15,40 26.575,39.4Q26,38.8 26,37.8V15.95Q26,15.15 26.575,14.575Q27.15,14 27.95,14H41.55Q42.55,14 43.275,14.575Q44,15.15 44,15.95V37.8Q44,38.8 43.275,39.4Q42.55,40 41.55,40ZM29,35.5H41V17H29Z"/>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="48dp"
|
|
||||||
android:height="48dp"
|
|
||||||
android:viewportWidth="48"
|
|
||||||
android:viewportHeight="48">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M31.3,21.35Q32.45,21.35 33.225,20.575Q34,19.8 34,18.65Q34,17.5 33.225,16.725Q32.45,15.95 31.3,15.95Q30.15,15.95 29.375,16.725Q28.6,17.5 28.6,18.65Q28.6,19.8 29.375,20.575Q30.15,21.35 31.3,21.35ZM16.7,21.35Q17.85,21.35 18.625,20.575Q19.4,19.8 19.4,18.65Q19.4,17.5 18.625,16.725Q17.85,15.95 16.7,15.95Q15.55,15.95 14.775,16.725Q14,17.5 14,18.65Q14,19.8 14.775,20.575Q15.55,21.35 16.7,21.35ZM24,34.95Q26.85,34.95 29.375,33.6Q31.9,32.25 33.35,29.85Q33.75,29.25 33.425,28.8Q33.1,28.35 32.4,28.35H15.6Q14.9,28.35 14.6,28.8Q14.3,29.25 14.7,29.85Q16.15,32.25 18.65,33.6Q21.15,34.95 24,34.95ZM24,44Q19.9,44 16.25,42.425Q12.6,40.85 9.875,38.125Q7.15,35.4 5.575,31.75Q4,28.1 4,23.95Q4,19.85 5.575,16.2Q7.15,12.55 9.875,9.85Q12.6,7.15 16.25,5.575Q19.9,4 24.05,4Q28.15,4 31.8,5.575Q35.45,7.15 38.15,9.85Q40.85,12.55 42.425,16.2Q44,19.85 44,24Q44,28.1 42.425,31.75Q40.85,35.4 38.15,38.125Q35.45,40.85 31.8,42.425Q28.15,44 24,44ZM24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24ZM24,41Q31.1,41 36.05,36.025Q41,31.05 41,24Q41,16.9 36.05,11.95Q31.1,7 24,7Q16.95,7 11.975,11.95Q7,16.9 7,24Q7,31.05 11.975,36.025Q16.95,41 24,41Z"/>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="48dp"
|
|
||||||
android:height="48dp"
|
|
||||||
android:viewportWidth="48"
|
|
||||||
android:viewportHeight="48">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M15.8,41H32.2V34.65Q32.2,31.15 29.825,28.625Q27.45,26.1 24,26.1Q20.55,26.1 18.175,28.625Q15.8,31.15 15.8,34.65ZM38.5,44H9.5Q8.85,44 8.425,43.575Q8,43.15 8,42.5Q8,41.85 8.425,41.425Q8.85,41 9.5,41H12.8V34.65Q12.8,31.15 14.625,28.225Q16.45,25.3 19.7,24Q16.45,22.7 14.625,19.75Q12.8,16.8 12.8,13.3V7H9.5Q8.85,7 8.425,6.575Q8,6.15 8,5.5Q8,4.85 8.425,4.425Q8.85,4 9.5,4H38.5Q39.15,4 39.575,4.425Q40,4.85 40,5.5Q40,6.15 39.575,6.575Q39.15,7 38.5,7H35.2V13.3Q35.2,16.8 33.35,19.75Q31.5,22.7 28.3,24Q31.55,25.3 33.375,28.225Q35.2,31.15 35.2,34.65V41H38.5Q39.15,41 39.575,41.425Q40,41.85 40,42.5Q40,43.15 39.575,43.575Q39.15,44 38.5,44Z"/>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="48dp"
|
|
||||||
android:height="48dp"
|
|
||||||
android:viewportWidth="48"
|
|
||||||
android:viewportHeight="48">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M24,24.6Q24.65,24.6 25.075,24.175Q25.5,23.75 25.5,23.1V15.75Q25.5,15.1 25.075,14.675Q24.65,14.25 24,14.25Q23.35,14.25 22.925,14.675Q22.5,15.1 22.5,15.75V23.1Q22.5,23.75 22.925,24.175Q23.35,24.6 24,24.6ZM24,31.3Q24.7,31.3 25.2,30.8Q25.7,30.3 25.7,29.6Q25.7,28.9 25.2,28.4Q24.7,27.9 24,27.9Q23.3,27.9 22.8,28.4Q22.3,28.9 22.3,29.6Q22.3,30.3 22.8,30.8Q23.3,31.3 24,31.3ZM24,43.85Q23.8,43.85 23.625,43.825Q23.45,43.8 23.3,43.75Q16.6,41.75 12.3,35.525Q8,29.3 8,21.85V12.05Q8,11.1 8.55,10.325Q9.1,9.55 9.95,9.2L22.95,4.35Q23.5,4.15 24,4.15Q24.5,4.15 25.05,4.35L38.05,9.2Q38.9,9.55 39.45,10.325Q40,11.1 40,12.05V21.85Q40,29.3 35.7,35.525Q31.4,41.75 24.7,43.75Q24.7,43.75 24,43.85ZM24,40.85Q29.75,38.95 33.375,33.675Q37,28.4 37,21.85V12.05Q37,12.05 37,12.05Q37,12.05 37,12.05L24,7.15Q24,7.15 24,7.15Q24,7.15 24,7.15L11,12.05Q11,12.05 11,12.05Q11,12.05 11,12.05V21.85Q11,28.4 14.625,33.675Q18.25,38.95 24,40.85ZM24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Z"/>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_identity_confirmation_subtitle">"Verify this device to set up secure messaging."</string>
|
||||||
|
<string name="screen_identity_confirmation_title">"Confirm that it\'s you"</string>
|
||||||
|
<string name="screen_identity_confirmed_subtitle">"Now you can read or send messages securely, and anyone you chat with can also trust this device."</string>
|
||||||
|
<string name="screen_identity_confirmed_title">"Device verified"</string>
|
||||||
|
<string name="screen_identity_use_another_device">"Use another device"</string>
|
||||||
|
<string name="screen_identity_waiting_on_other_device">"Waiting on other device…"</string>
|
||||||
<string name="screen_session_verification_cancelled_subtitle">"Something doesn’t seem right. Either the request timed out or the request was denied."</string>
|
<string name="screen_session_verification_cancelled_subtitle">"Something doesn’t seem right. Either the request timed out or the request was denied."</string>
|
||||||
<string name="screen_session_verification_compare_emojis_subtitle">"Confirm that the emojis below match those shown on your other session."</string>
|
<string name="screen_session_verification_compare_emojis_subtitle">"Confirm that the emojis below match those shown on your other session."</string>
|
||||||
<string name="screen_session_verification_compare_emojis_title">"Compare emojis"</string>
|
<string name="screen_session_verification_compare_emojis_title">"Compare emojis"</string>
|
||||||
|
|
|
||||||
|
|
@ -106,13 +106,13 @@ class VerifySelfSessionPresenterTests {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
|
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
|
||||||
val eventSink = initialState.eventSink
|
val eventSink = initialState.eventSink
|
||||||
eventSink(VerifySelfSessionViewEvents.CancelAndClose)
|
eventSink(VerifySelfSessionViewEvents.Cancel)
|
||||||
expectNoEvents()
|
expectNoEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - A fail in the flow cancels it`() = runTest {
|
fun `present - A failure when verifying cancels it`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val service = FakeSessionVerificationService()
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -128,6 +128,21 @@ class VerifySelfSessionPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - A fail when requesting verification resets the state to the initial one`() = runTest {
|
||||||
|
val service = FakeSessionVerificationService()
|
||||||
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
service.shouldFail = true
|
||||||
|
awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
||||||
|
service.shouldFail = false
|
||||||
|
assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.AwaitingOtherDeviceResponse::class.java)
|
||||||
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Canceling the flow once it's verifying cancels it`() = runTest {
|
fun `present - Canceling the flow once it's verifying cancels it`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val service = FakeSessionVerificationService()
|
||||||
|
|
@ -136,8 +151,7 @@ class VerifySelfSessionPresenterTests {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
state.eventSink(VerifySelfSessionViewEvents.CancelAndClose)
|
state.eventSink(VerifySelfSessionViewEvents.Cancel)
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
|
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,13 +179,30 @@ class VerifySelfSessionPresenterTests {
|
||||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
service.givenVerificationFlowState(VerificationFlowState.Canceled)
|
service.givenVerificationFlowState(VerificationFlowState.Canceled)
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
||||||
state.eventSink(VerifySelfSessionViewEvents.Restart)
|
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
||||||
// Went back to requesting verification
|
// Went back to requesting verification
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Go back after cancelation returns to initial state`() = runTest {
|
||||||
|
val service = FakeSessionVerificationService()
|
||||||
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
|
service.givenVerificationFlowState(VerificationFlowState.Canceled)
|
||||||
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
||||||
|
state.eventSink(VerifySelfSessionViewEvents.Reset)
|
||||||
|
// Went back to initial state
|
||||||
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - When verification is approved, the flow completes if there is no error`() = runTest {
|
fun `present - When verification is approved, the flow completes if there is no error`() = runTest {
|
||||||
val emojis = listOf(
|
val emojis = listOf(
|
||||||
|
|
@ -247,7 +278,7 @@ class VerifySelfSessionPresenterTests {
|
||||||
return VerifySelfSessionPresenter(
|
return VerifySelfSessionPresenter(
|
||||||
sessionVerificationService = service,
|
sessionVerificationService = service,
|
||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
stateMachine = VerifySelfSessionStateMachine(service),
|
stateMachine = VerifySelfSessionStateMachine(service, encryptionService),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,45 +36,98 @@ class VerifySelfSessionViewTest {
|
||||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on cancel calls the expected callback and emits the expected Event`() {
|
fun `back key pressed - when canceled resets the flow`() {
|
||||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||||
ensureCalledOnce { callback ->
|
rule.setContent {
|
||||||
rule.setContent {
|
VerifySelfSessionView(
|
||||||
VerifySelfSessionView(
|
aVerifySelfSessionState(
|
||||||
aVerifySelfSessionState(
|
verificationFlowStep = VerifySelfSessionState.VerificationStep.Canceled,
|
||||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
|
eventSink = eventsRecorder
|
||||||
eventSink = eventsRecorder
|
),
|
||||||
),
|
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
onFinished = EnsureNeverCalled(),
|
||||||
goBack = callback,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
rule.clickOn(CommonStrings.action_cancel)
|
|
||||||
}
|
}
|
||||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose)
|
rule.pressBackKey()
|
||||||
|
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Reset)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on back key calls the expected callback and emits the expected Event`() {
|
fun `back key pressed - when awaiting response cancels the verification`() {
|
||||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||||
ensureCalledOnce { callback ->
|
rule.setContent {
|
||||||
rule.setContent {
|
VerifySelfSessionView(
|
||||||
VerifySelfSessionView(
|
aVerifySelfSessionState(
|
||||||
aVerifySelfSessionState(
|
verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse,
|
||||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
|
eventSink = eventsRecorder
|
||||||
eventSink = eventsRecorder
|
),
|
||||||
),
|
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
onFinished = EnsureNeverCalled(),
|
||||||
goBack = callback,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
rule.pressBackKey()
|
|
||||||
}
|
}
|
||||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose)
|
rule.pressBackKey()
|
||||||
|
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when flow is completed, the expected callback is invoked`() {
|
fun `back key pressed - when ready to verify cancels the verification`() {
|
||||||
|
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||||
|
rule.setContent {
|
||||||
|
VerifySelfSessionView(
|
||||||
|
aVerifySelfSessionState(
|
||||||
|
verificationFlowStep = VerifySelfSessionState.VerificationStep.Ready,
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||||
|
onFinished = EnsureNeverCalled(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rule.pressBackKey()
|
||||||
|
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `back key pressed - when verifying and not loading declines the verification`() {
|
||||||
|
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||||
|
rule.setContent {
|
||||||
|
VerifySelfSessionView(
|
||||||
|
aVerifySelfSessionState(
|
||||||
|
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
|
||||||
|
data = aEmojisSessionVerificationData(),
|
||||||
|
state = AsyncData.Uninitialized,
|
||||||
|
),
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||||
|
onFinished = EnsureNeverCalled(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rule.pressBackKey()
|
||||||
|
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `back key pressed - when verifying and loading does nothing`() {
|
||||||
|
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||||
|
rule.setContent {
|
||||||
|
VerifySelfSessionView(
|
||||||
|
aVerifySelfSessionState(
|
||||||
|
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
|
||||||
|
data = aEmojisSessionVerificationData(),
|
||||||
|
state = AsyncData.Loading(),
|
||||||
|
),
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||||
|
onFinished = EnsureNeverCalled(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rule.pressBackKey()
|
||||||
|
eventsRecorder.assertEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() {
|
||||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>(expectEvents = false)
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setContent {
|
rule.setContent {
|
||||||
|
|
@ -84,9 +137,10 @@ class VerifySelfSessionViewTest {
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||||
goBack = callback,
|
onFinished = callback,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
rule.clickOn(CommonStrings.action_continue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +156,7 @@ class VerifySelfSessionViewTest {
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
onEnterRecoveryKey = callback,
|
onEnterRecoveryKey = callback,
|
||||||
goBack = EnsureNeverCalled(),
|
onFinished = EnsureNeverCalled(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rule.clickOn(R.string.screen_session_verification_enter_recovery_key)
|
rule.clickOn(R.string.screen_session_verification_enter_recovery_key)
|
||||||
|
|
@ -122,7 +176,7 @@ class VerifySelfSessionViewTest {
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||||
goBack = EnsureNeverCalled(),
|
onFinished = EnsureNeverCalled(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rule.clickOn(R.string.screen_session_verification_they_match)
|
rule.clickOn(R.string.screen_session_verification_they_match)
|
||||||
|
|
@ -142,7 +196,7 @@ class VerifySelfSessionViewTest {
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||||
goBack = EnsureNeverCalled(),
|
onFinished = EnsureNeverCalled(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rule.clickOn(R.string.screen_session_verification_they_dont_match)
|
rule.clickOn(R.string.screen_session_verification_they_dont_match)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
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.atoms.RoundedIconAtomSize
|
||||||
|
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
@ -95,3 +96,14 @@ internal fun IconTitleSubtitleMoleculePreview() = ElementPreview {
|
||||||
subTitle = "Subtitle",
|
subTitle = "Subtitle",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun IconTitleSubtitleMoleculeWithResIconPreview() = ElementPreview {
|
||||||
|
IconTitleSubtitleMolecule(
|
||||||
|
iconResourceId = CompoundDrawables.ic_compound_admin,
|
||||||
|
iconTint = Color.Black,
|
||||||
|
title = "Title",
|
||||||
|
subTitle = "Subtitle",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,7 @@ fun PageTitle(
|
||||||
callToAction: @Composable (() -> Unit)? = null,
|
callToAction: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 40.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
BigIcon(style = iconStyle)
|
BigIcon(style = iconStyle)
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,9 @@ sealed interface SessionVerifiedStatus {
|
||||||
|
|
||||||
/** Verified session status. */
|
/** Verified session status. */
|
||||||
data object Verified : SessionVerifiedStatus
|
data object Verified : SessionVerifiedStatus
|
||||||
|
|
||||||
|
/** Returns whether the session is [Verified]. */
|
||||||
|
fun isVerified(): Boolean = this is Verified
|
||||||
}
|
}
|
||||||
|
|
||||||
/** States produced by the [SessionVerificationService]. */
|
/** States produced by the [SessionVerificationService]. */
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryServic
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
|
@ -84,8 +85,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -130,11 +130,6 @@ class RustMatrixClient(
|
||||||
private val innerRoomListService = syncService.roomListService()
|
private val innerRoomListService = syncService.roomListService()
|
||||||
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
||||||
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
|
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
|
||||||
private val verificationService = RustSessionVerificationService(
|
|
||||||
client = client,
|
|
||||||
syncService = rustSyncService,
|
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
|
||||||
).apply { start() }
|
|
||||||
private val pushersService = RustPushersService(
|
private val pushersService = RustPushersService(
|
||||||
client = client,
|
client = client,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
|
|
@ -230,6 +225,12 @@ class RustMatrixClient(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val verificationService = RustSessionVerificationService(
|
||||||
|
client = client,
|
||||||
|
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
|
||||||
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
|
)
|
||||||
|
|
||||||
private val eventFilters = TimelineConfig.excludedEvents
|
private val eventFilters = TimelineConfig.excludedEvents
|
||||||
.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
?.let { listStateEventType ->
|
?.let { listStateEventType ->
|
||||||
|
|
@ -274,11 +275,6 @@ class RustMatrixClient(
|
||||||
.stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf())
|
.stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
roomListService.state.onEach { state ->
|
|
||||||
if (state == RoomListService.State.Running) {
|
|
||||||
setupVerificationControllerIfNeeded()
|
|
||||||
}
|
|
||||||
}.launchIn(sessionCoroutineScope)
|
|
||||||
sessionCoroutineScope.launch {
|
sessionCoroutineScope.launch {
|
||||||
// Force a refresh of the profile
|
// Force a refresh of the profile
|
||||||
getUserProfile()
|
getUserProfile()
|
||||||
|
|
@ -490,6 +486,7 @@ class RustMatrixClient(
|
||||||
ignoreSdkError: Boolean,
|
ignoreSdkError: Boolean,
|
||||||
): String? {
|
): String? {
|
||||||
var result: String? = null
|
var result: String? = null
|
||||||
|
syncService.stop()
|
||||||
withContext(sessionDispatcher) {
|
withContext(sessionDispatcher) {
|
||||||
if (doRequest) {
|
if (doRequest) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -525,16 +522,6 @@ class RustMatrixClient(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupVerificationControllerIfNeeded() {
|
|
||||||
if (verificationService.verificationController == null) {
|
|
||||||
try {
|
|
||||||
verificationService.verificationController = client.getSessionVerificationController()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.e(e, "Could not start verification service. Will try again on the next sliding sync update.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
||||||
|
|
||||||
private suspend fun File.getCacheSize(
|
private suspend fun File.getCacheSize(
|
||||||
|
|
|
||||||
|
|
@ -16,92 +16,135 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.verification
|
package io.element.android.libraries.matrix.impl.verification
|
||||||
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
|
||||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import org.matrix.rustcomponents.sdk.Client
|
import org.matrix.rustcomponents.sdk.Client
|
||||||
import org.matrix.rustcomponents.sdk.Encryption
|
import org.matrix.rustcomponents.sdk.Encryption
|
||||||
import org.matrix.rustcomponents.sdk.RecoveryState
|
import org.matrix.rustcomponents.sdk.RecoveryState
|
||||||
import org.matrix.rustcomponents.sdk.RecoveryStateListener
|
import org.matrix.rustcomponents.sdk.RecoveryStateListener
|
||||||
import org.matrix.rustcomponents.sdk.SessionVerificationController
|
import org.matrix.rustcomponents.sdk.SessionVerificationController
|
||||||
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
|
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
|
||||||
import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface
|
|
||||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||||
|
import org.matrix.rustcomponents.sdk.VerificationState
|
||||||
|
import org.matrix.rustcomponents.sdk.VerificationStateListener
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
|
import timber.log.Timber
|
||||||
import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData
|
import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData
|
||||||
|
|
||||||
class RustSessionVerificationService(
|
class RustSessionVerificationService(
|
||||||
client: Client,
|
client: Client,
|
||||||
private val syncService: RustSyncService,
|
isSyncServiceReady: Flow<Boolean>,
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
sessionCoroutineScope: CoroutineScope,
|
||||||
) : SessionVerificationService, SessionVerificationControllerDelegate {
|
) : SessionVerificationService, SessionVerificationControllerDelegate {
|
||||||
|
private var verificationStateListenerTaskHandle: TaskHandle? = null
|
||||||
private var recoveryStateListenerTaskHandle: TaskHandle? = null
|
private var recoveryStateListenerTaskHandle: TaskHandle? = null
|
||||||
private val encryptionService: Encryption = client.encryption()
|
private val encryptionService: Encryption = client.encryption()
|
||||||
var verificationController: SessionVerificationControllerInterface? = null
|
private lateinit var verificationController: SessionVerificationController
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
_isReady.value = value != null
|
|
||||||
// If status was 'Unknown', move it to either 'Verified' or 'NotVerified'
|
|
||||||
if (value != null) {
|
|
||||||
value.setDelegate(this)
|
|
||||||
sessionCoroutineScope.launch { updateVerificationStatus(value.isVerified()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
private val _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
||||||
override val verificationFlowState = _verificationFlowState.asStateFlow()
|
override val verificationFlowState = _verificationFlowState.asStateFlow()
|
||||||
|
|
||||||
private val _isReady = MutableStateFlow(false)
|
|
||||||
override val isReady = _isReady.asStateFlow()
|
|
||||||
|
|
||||||
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
||||||
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
|
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
|
||||||
|
|
||||||
override val canVerifySessionFlow = combine(sessionVerifiedStatus, syncService.syncState) { verificationStatus, syncState ->
|
override val isReady = MutableStateFlow(false)
|
||||||
syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified
|
|
||||||
|
override val canVerifySessionFlow = combine(sessionVerifiedStatus, isReady) { verificationStatus, isReady ->
|
||||||
|
isReady && verificationStatus == SessionVerifiedStatus.NotVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
init {
|
||||||
recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener {
|
isSyncServiceReady
|
||||||
override fun onUpdate(status: RecoveryState) {
|
.onEach { syncServiceReady ->
|
||||||
sessionCoroutineScope.launch {
|
if (syncServiceReady) {
|
||||||
updateVerificationStatus(verificationController?.isVerified().orFalse())
|
isReady.value = true
|
||||||
|
runCatching {
|
||||||
|
// If the controller was failed to initialize before, we try to get it again
|
||||||
|
if (!this::verificationController.isInitialized) {
|
||||||
|
verificationController = client.getSessionVerificationController()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
isReady.value = false
|
||||||
|
Timber.e(it, "Failed to get verification controller. Trying again in next sync.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isReady.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
.launchIn(sessionCoroutineScope)
|
||||||
|
|
||||||
|
isReady.onEach { isReady ->
|
||||||
|
if (isReady) {
|
||||||
|
Timber.d("Starting verification service")
|
||||||
|
// Setup delegate
|
||||||
|
verificationController.setDelegate(this)
|
||||||
|
|
||||||
|
// Immediate status update
|
||||||
|
updateVerificationStatus(encryptionService.verificationState())
|
||||||
|
|
||||||
|
// Listen for changes in verification status and update accordingly
|
||||||
|
verificationStateListenerTaskHandle?.cancelAndDestroy()
|
||||||
|
verificationStateListenerTaskHandle = encryptionService.verificationStateListener(object : VerificationStateListener {
|
||||||
|
override fun onUpdate(status: VerificationState) {
|
||||||
|
Timber.d("New verification state: $status")
|
||||||
|
updateVerificationStatus(status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// In case we enter the recovery key instead we check changes in the recovery state, since the listener above won't be triggered
|
||||||
|
recoveryStateListenerTaskHandle?.cancelAndDestroy()
|
||||||
|
recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener {
|
||||||
|
override fun onUpdate(status: RecoveryState) {
|
||||||
|
Timber.d("New recovery state: $status")
|
||||||
|
// We could check the `RecoveryState`, but it's easier to just use the verification state directly
|
||||||
|
updateVerificationStatus(encryptionService.verificationState())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Timber.d("Stopping verification service")
|
||||||
|
if (this::verificationController.isInitialized) {
|
||||||
|
verificationController.setDelegate(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(sessionCoroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun requestVerification() = tryOrFail {
|
override suspend fun requestVerification() = tryOrFail {
|
||||||
verificationController?.requestVerification()
|
verificationController.requestVerification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun cancelVerification() = tryOrFail { verificationController?.cancelVerification() }
|
override suspend fun cancelVerification() = tryOrFail { verificationController.cancelVerification() }
|
||||||
|
|
||||||
override suspend fun approveVerification() = tryOrFail { verificationController?.approveVerification() }
|
override suspend fun approveVerification() = tryOrFail { verificationController.approveVerification() }
|
||||||
|
|
||||||
override suspend fun declineVerification() = tryOrFail { verificationController?.declineVerification() }
|
override suspend fun declineVerification() = tryOrFail { verificationController.declineVerification() }
|
||||||
|
|
||||||
override suspend fun startVerification() = tryOrFail {
|
override suspend fun startVerification() = tryOrFail {
|
||||||
verificationController?.startSasVerification()
|
verificationController.startSasVerification()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun tryOrFail(block: suspend () -> Unit) {
|
private suspend fun tryOrFail(block: suspend () -> Unit) {
|
||||||
runCatching {
|
runCatching {
|
||||||
block()
|
block()
|
||||||
}.onFailure { didFail() }
|
}.onFailure {
|
||||||
|
Timber.e(it, "Failed to verify session")
|
||||||
|
didFail()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Delegate implementation
|
// region Delegate implementation
|
||||||
|
|
@ -116,13 +159,14 @@ class RustSessionVerificationService(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didFail() {
|
override fun didFail() {
|
||||||
|
Timber.e("Session verification failed with an unknown error")
|
||||||
_verificationFlowState.value = VerificationFlowState.Failed
|
_verificationFlowState.value = VerificationFlowState.Failed
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didFinish() {
|
override fun didFinish() {
|
||||||
_verificationFlowState.value = VerificationFlowState.Finished
|
_verificationFlowState.value = VerificationFlowState.Finished
|
||||||
// Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it always returns false
|
// Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it always returns false
|
||||||
updateVerificationStatus(isVerified = true)
|
updateVerificationStatus(VerificationState.VERIFIED)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveVerificationData(data: RustSessionVerificationData) {
|
override fun didReceiveVerificationData(data: RustSessionVerificationData) {
|
||||||
|
|
@ -139,25 +183,26 @@ class RustSessionVerificationService(
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
if (isReady.value) {
|
if (isReady.value) {
|
||||||
// Cancel any pending verification attempt
|
// Cancel any pending verification attempt
|
||||||
tryOrNull { verificationController?.cancelVerification() }
|
tryOrNull { verificationController.cancelVerification() }
|
||||||
}
|
}
|
||||||
_verificationFlowState.value = VerificationFlowState.Initial
|
_verificationFlowState.value = VerificationFlowState.Initial
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
|
Timber.d("Destroying RustSessionVerificationService")
|
||||||
recoveryStateListenerTaskHandle?.cancelAndDestroy()
|
recoveryStateListenerTaskHandle?.cancelAndDestroy()
|
||||||
verificationController?.setDelegate(null)
|
if (this::verificationController.isInitialized) {
|
||||||
(verificationController as? SessionVerificationController)?.destroy()
|
verificationController.setDelegate(null)
|
||||||
verificationController = null
|
(verificationController as? SessionVerificationController)?.destroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateVerificationStatus(isVerified: Boolean) {
|
private fun updateVerificationStatus(verificationState: VerificationState) {
|
||||||
val newValue = when {
|
_sessionVerifiedStatus.value = when (verificationState) {
|
||||||
!isReady.value -> SessionVerifiedStatus.Unknown
|
VerificationState.UNKNOWN -> SessionVerifiedStatus.Unknown
|
||||||
!isVerified -> SessionVerifiedStatus.NotVerified
|
VerificationState.VERIFIED -> SessionVerifiedStatus.Verified
|
||||||
else -> SessionVerifiedStatus.Verified
|
VerificationState.UNVERIFIED -> SessionVerifiedStatus.NotVerified
|
||||||
}
|
}
|
||||||
_sessionVerifiedStatus.value = newValue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,11 @@ class FakeSessionVerificationService : SessionVerificationService {
|
||||||
override val isReady: StateFlow<Boolean> = _isReady
|
override val isReady: StateFlow<Boolean> = _isReady
|
||||||
|
|
||||||
override suspend fun requestVerification() {
|
override suspend fun requestVerification() {
|
||||||
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
|
if (!shouldFail) {
|
||||||
|
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
|
||||||
|
} else {
|
||||||
|
_verificationFlowState.value = VerificationFlowState.Failed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun cancelVerification() {
|
override suspend fun cancelVerification() {
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,8 @@
|
||||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||||
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
||||||
|
<string name="screen_room_directory_search_loading_error">"Failed loading"</string>
|
||||||
|
<string name="screen_room_directory_search_title">"Room directory"</string>
|
||||||
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
|
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
|
||||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||||
<string name="screen_room_member_details_block_alert_action">"Block"</string>
|
<string name="screen_room_member_details_block_alert_action">"Block"</string>
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,6 @@ class RoomListScreen(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClicked = ::onRoomClicked,
|
onRoomClicked = ::onRoomClicked,
|
||||||
onSettingsClicked = {},
|
onSettingsClicked = {},
|
||||||
onVerifyClicked = {},
|
|
||||||
onConfirmRecoveryKeyClicked = {},
|
onConfirmRecoveryKeyClicked = {},
|
||||||
onCreateRoomClicked = {},
|
onCreateRoomClicked = {},
|
||||||
onInvitesClicked = {},
|
onInvitesClicked = {},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:455964d5b4d1652b5b9eeb8b7236f14c2c9490123399e8c65f1b9e920d20071d
|
|
||||||
size 21053
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:84d78e6672aaafd2c7974630e7d9391ee7e649c9a9de470627d1ed6e1241a1f4
|
|
||||||
size 13366
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:c90e7168839f33c8ea4ef79308cc2b1c7a1ad4ec07dbeedba14dd080a3a841d8
|
|
||||||
size 20169
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0628f8cd42815e725efc8f7bfe0bc790c2369f8d4cabfd31aabd494a469f7379
|
|
||||||
size 12867
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:be4d708775d03680be450dfd5eebfeda24f1841eedced3e7de2ba036669ac479
|
oid sha256:75fb611e02345fe1cf7947d1e160e309cf8520f0256ebd25b43bab8c3102f3f8
|
||||||
size 39086
|
size 37138
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:53660f4354d2a750ab539370151c9ee4ac49f9d06712f7646fdd487bf133753a
|
oid sha256:a7be4df5a4e391d2253f283acf0fb9362b965fa887235221b794915d74891899
|
||||||
size 38766
|
size 36783
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8ae1828957e00adcbdbb772a0ebdc1fb6604afbe644cc5d8662da9fcd3973129
|
oid sha256:67503a61f560ef79d7e981057094dccf46eda167ee84427c1aceb9f83edf0146
|
||||||
size 41162
|
size 39080
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:58e567815b4dc36abf8422b17f9902d8581dbec1a1ed216df2f120c6198b6fe5
|
oid sha256:5cb3be70de8e3878bdd90db9945aed60fb2b38d8c66ea610806226c21b1f5a5a
|
||||||
size 41124
|
size 39052
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f5fd867f51badedc8e9ded0900eb904ac11af348243573dcf34b8aaa3c1548b7
|
|
||||||
size 26926
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:318838247c7d0cee133fd5c98bbbee6f2c5be09a95b6c2fd293c041e21480ab8
|
|
||||||
size 26080
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue