First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.features.logout.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.workmanager.api)
api(projects.features.logout.api)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.workmanager.test)
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultLogoutEntryPoint : LogoutEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: LogoutEntryPoint.Callback,
): Node {
return parentNode.createNode<LogoutNode>(buildContext, listOf(callback))
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.SessionStore
import timber.log.Timber
@ContributesBinding(AppScope::class)
class DefaultLogoutUseCase(
private val sessionStore: SessionStore,
private val matrixClientProvider: MatrixClientProvider,
) : LogoutUseCase {
override suspend fun logoutAll(ignoreSdkError: Boolean) {
sessionStore.getAllSessions()
.map { sessionData ->
SessionId(sessionData.userId)
}
.forEach { sessionId ->
Timber.d("Logging out sessionId: $sessionId")
matrixClientProvider.getOrRestore(sessionId).fold(
onSuccess = { client ->
client.logout(userInitiated = true, ignoreSdkError = ignoreSdkError)
},
onFailure = { error ->
Timber.e(error, "Failed to get or restore MatrixClient for sessionId: $sessionId")
}
)
}
}
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
sealed interface LogoutEvents {
data class Logout(val ignoreSdkError: Boolean) : LogoutEvents
data object CloseDialogs : LogoutEvents
}
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class LogoutNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: LogoutPresenter,
) : Node(buildContext, plugins = plugins) {
private val callback: LogoutEntryPoint.Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LogoutView(
state = state,
onChangeRecoveryKeyClick = callback::navigateToSecureBackup,
onBackClick = ::navigateUp,
modifier = modifier,
)
}
}
@@ -0,0 +1,121 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Inject
class LogoutPresenter(
private val matrixClient: MatrixClient,
private val encryptionService: EncryptionService,
private val workManagerScheduler: WorkManagerScheduler,
) : Presenter<LogoutState> {
@Composable
override fun present(): LogoutState {
val localCoroutineScope = rememberCoroutineScope()
val logoutAction: MutableState<AsyncAction<Unit>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
val backupUploadState: BackupUploadState by remember {
encryptionService.waitForBackupUploadSteadyState()
}
.collectAsState(initial = BackupUploadState.Unknown)
var waitingForALongTime by remember { mutableStateOf(false) }
LaunchedEffect(backupUploadState) {
if (backupUploadState is BackupUploadState.Waiting) {
delay(2_000)
waitingForALongTime = true
} else {
waitingForALongTime = false
}
}
val isLastDevice by encryptionService.isLastDevice.collectAsState()
val backupState by encryptionService.backupStateStateFlow.collectAsState()
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val doesBackupExistOnServerAction: MutableState<AsyncData<Boolean>> = remember {
mutableStateOf(AsyncData.Uninitialized)
}
LaunchedEffect(backupState) {
if (backupState == BackupState.UNKNOWN) {
getKeyBackupStatus(doesBackupExistOnServerAction)
}
}
fun handleEvent(event: LogoutEvents) {
when (event) {
is LogoutEvents.Logout -> {
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
} else {
logoutAction.value = AsyncAction.ConfirmingNoParams
}
}
LogoutEvents.CloseDialogs -> {
logoutAction.value = AsyncAction.Uninitialized
}
}
}
return LogoutState(
isLastDevice = isLastDevice,
backupState = backupState,
doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(),
recoveryState = recoveryState,
backupUploadState = backupUploadState,
waitingForALongTime = waitingForALongTime,
logoutAction = logoutAction.value,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.getKeyBackupStatus(action: MutableState<AsyncData<Boolean>>) = launch {
suspend {
encryptionService.doesBackupExistOnServer().getOrThrow()
}.runCatchingUpdatingState(action)
}
private fun CoroutineScope.logout(
logoutAction: MutableState<AsyncAction<Unit>>,
ignoreSdkError: Boolean,
) = launch {
suspend {
// Cancel any pending work (e.g. notification sync)
workManagerScheduler.cancel(matrixClient.sessionId)
matrixClient.logout(userInitiated = true, ignoreSdkError)
}.runCatchingUpdatingState(logoutAction)
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.RecoveryState
data class LogoutState(
val isLastDevice: Boolean,
val backupState: BackupState,
val doesBackupExistOnServer: Boolean,
val recoveryState: RecoveryState,
val backupUploadState: BackupUploadState,
val waitingForALongTime: Boolean,
val logoutAction: AsyncAction<Unit>,
val eventSink: (LogoutEvents) -> Unit,
)
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.encryption.SteadyStateException
open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
override val values: Sequence<LogoutState>
get() = sequenceOf(
aLogoutState(),
aLogoutState(isLastDevice = true),
aLogoutState(isLastDevice = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
aLogoutState(isLastDevice = true, backupUploadState = BackupUploadState.Done),
aLogoutState(logoutAction = AsyncAction.ConfirmingNoParams),
aLogoutState(logoutAction = AsyncAction.Loading),
aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))),
aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),
// Last session no recovery
aLogoutState(isLastDevice = true, recoveryState = RecoveryState.DISABLED),
// Last session no backup
aLogoutState(isLastDevice = true, backupState = BackupState.UNKNOWN, doesBackupExistOnServer = false),
aLogoutState(
isLastDevice = false,
backupUploadState = BackupUploadState.Waiting,
),
aLogoutState(
isLastDevice = false,
backupUploadState = BackupUploadState.Waiting,
waitingForALongTime = true,
),
)
}
fun aLogoutState(
isLastDevice: Boolean = false,
backupState: BackupState = BackupState.ENABLED,
doesBackupExistOnServer: Boolean = true,
recoveryState: RecoveryState = RecoveryState.ENABLED,
backupUploadState: BackupUploadState = BackupUploadState.Unknown,
waitingForALongTime: Boolean = false,
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (LogoutEvents) -> Unit = {},
) = LogoutState(
isLastDevice = isLastDevice,
backupState = backupState,
doesBackupExistOnServer = doesBackupExistOnServer,
recoveryState = recoveryState,
backupUploadState = backupUploadState,
waitingForALongTime = waitingForALongTime,
logoutAction = logoutAction,
eventSink = eventSink,
)
@@ -0,0 +1,196 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.logout.impl.tools.isBackingUp
import io.element.android.features.logout.impl.ui.LogoutActionDialog
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview
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.LinearProgressIndicator
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.encryption.SteadyStateException
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LogoutView(
state: LogoutState,
onChangeRecoveryKeyClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val eventSink = state.eventSink
FlowStepPage(
onBackClick = onBackClick,
title = title(state),
subTitle = subtitle(state),
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
modifier = modifier,
buttons = {
Buttons(
state = state,
onChangeRecoveryKeyClick = onChangeRecoveryKeyClick,
onLogoutClick = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
}
)
},
) {
Content(state)
}
LogoutActionDialog(
state.logoutAction,
onConfirmClick = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
},
onForceLogoutClick = {
eventSink(LogoutEvents.Logout(ignoreSdkError = true))
},
onDismissDialog = {
eventSink(LogoutEvents.CloseDialogs)
},
)
}
@Composable
private fun title(state: LogoutState): String {
return when {
state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title)
state.isLastDevice -> {
if (state.recoveryState != RecoveryState.ENABLED) {
stringResource(id = R.string.screen_signout_recovery_disabled_title)
} else if (state.backupState == BackupState.UNKNOWN && state.doesBackupExistOnServer.not()) {
stringResource(id = R.string.screen_signout_key_backup_disabled_title)
} else {
stringResource(id = R.string.screen_signout_save_recovery_key_title)
}
}
else -> stringResource(CommonStrings.action_signout)
}
}
@Composable
private fun subtitle(state: LogoutState): String? {
return when {
(state.backupUploadState as? BackupUploadState.SteadyException)?.exception is SteadyStateException.Connection ->
stringResource(id = R.string.screen_signout_key_backup_offline_subtitle)
state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle)
state.isLastDevice -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle)
else -> null
}
}
@Composable
private fun ColumnScope.Buttons(
state: LogoutState,
onLogoutClick: () -> Unit,
onChangeRecoveryKeyClick: () -> Unit,
) {
val logoutAction = state.logoutAction
if (state.isLastDevice) {
OutlinedButton(
text = stringResource(id = CommonStrings.common_settings),
modifier = Modifier.fillMaxWidth(),
onClick = onChangeRecoveryKeyClick,
)
}
val signOutSubmitRes = when {
logoutAction is AsyncAction.Loading -> R.string.screen_signout_in_progress_dialog_content
state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway
else -> CommonStrings.action_signout
}
Button(
text = stringResource(id = signOutSubmitRes),
showProgress = logoutAction is AsyncAction.Loading,
destructive = true,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.signOut),
onClick = onLogoutClick,
)
}
@Composable
private fun Content(
state: LogoutState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(top = 60.dp, start = 20.dp, end = 20.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
when (state.backupUploadState) {
is BackupUploadState.Uploading -> {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = { state.backupUploadState.backedUpCount.toFloat() / state.backupUploadState.totalCount.toFloat() },
trackColor = ElementTheme.colors.progressIndicatorTrackColor,
)
Text(
modifier = Modifier.align(Alignment.End),
text = "${state.backupUploadState.backedUpCount} / ${state.backupUploadState.totalCount}",
style = ElementTheme.typography.fontBodySmRegular,
)
}
BackupUploadState.Waiting -> {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
trackColor = ElementTheme.colors.progressIndicatorTrackColor,
)
if (state.waitingForALongTime) {
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = stringResource(CommonStrings.common_please_check_internet_connection),
style = ElementTheme.typography.fontBodySmRegular,
)
}
}
else -> Unit
}
}
}
@PreviewsDayNight
@Composable
internal fun LogoutViewPreview(
@PreviewParameter(LogoutStateProvider::class) state: LogoutState,
) = ElementPreview {
LogoutView(
state,
onChangeRecoveryKeyClick = {},
onBackClick = {},
)
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.impl.direct.DirectLogoutPresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
@BindingContainer
interface LogoutModule {
@Binds
fun bindDirectLogoutPresenter(presenter: DirectLogoutPresenter): Presenter<DirectLogoutState>
}
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl.direct
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameter
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.DirectLogoutStateProvider
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.logout.impl.ui.LogoutActionDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.di.SessionScope
@ContributesBinding(SessionScope::class)
class DefaultDirectLogoutView : DirectLogoutView {
@Composable
override fun Render(state: DirectLogoutState) {
val eventSink = state.eventSink
LogoutActionDialog(
state.logoutAction,
onConfirmClick = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
},
onForceLogoutClick = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true))
},
onDismissDialog = {
eventSink(DirectLogoutEvents.CloseDialogs)
},
)
}
}
@PreviewsDayNight
@Composable
internal fun DefaultDirectLogoutViewPreview(
@PreviewParameter(DirectLogoutStateProvider::class) state: DirectLogoutState,
) = ElementPreview {
DefaultDirectLogoutView().Render(state = state)
}
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl.direct
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.impl.tools.isBackingUp
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Inject
class DirectLogoutPresenter(
private val matrixClient: MatrixClient,
private val encryptionService: EncryptionService,
) : Presenter<DirectLogoutState> {
@Composable
override fun present(): DirectLogoutState {
val localCoroutineScope = rememberCoroutineScope()
val logoutAction: MutableState<AsyncAction<Unit>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
val backupUploadState: BackupUploadState by remember {
encryptionService.waitForBackupUploadSteadyState()
}
.collectAsState(initial = BackupUploadState.Unknown)
val isLastDevice by encryptionService.isLastDevice.collectAsState()
fun handleEvent(event: DirectLogoutEvents) {
when (event) {
is DirectLogoutEvents.Logout -> {
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
} else {
logoutAction.value = AsyncAction.ConfirmingNoParams
}
}
DirectLogoutEvents.CloseDialogs -> {
logoutAction.value = AsyncAction.Uninitialized
}
}
}
return DirectLogoutState(
canDoDirectSignOut = !isLastDevice &&
!backupUploadState.isBackingUp(),
logoutAction = logoutAction.value,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.logout(
logoutAction: MutableState<AsyncAction<Unit>>,
ignoreSdkError: Boolean,
) = launch {
suspend {
matrixClient.logout(userInitiated = true, ignoreSdkError)
}.runCatchingUpdatingState(logoutAction)
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl.tools
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.SteadyStateException
internal fun BackupUploadState.isBackingUp(): Boolean {
return when (this) {
BackupUploadState.Waiting,
is BackupUploadState.Uploading -> true
// The backup is in progress, but there have been a network issue, so we have to warn the user.
is BackupUploadState.SteadyException -> exception is SteadyStateException.Connection
BackupUploadState.Unknown,
BackupUploadState.Done,
BackupUploadState.Error -> false
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.features.logout.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LogoutActionDialog(
state: AsyncAction<Unit>,
onConfirmClick: () -> Unit,
onForceLogoutClick: () -> Unit,
onDismissDialog: () -> Unit,
) {
when (state) {
AsyncAction.Uninitialized ->
Unit
is AsyncAction.Confirming ->
LogoutConfirmationDialog(
onSubmitClick = onConfirmClick,
onDismiss = onDismissDialog
)
is AsyncAction.Loading ->
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
is AsyncAction.Failure ->
RetryDialog(
title = stringResource(id = CommonStrings.dialog_title_error),
content = stringResource(id = CommonStrings.error_unknown),
retryText = stringResource(id = CommonStrings.action_signout_anyway),
onRetry = onForceLogoutClick,
onDismiss = onDismissDialog,
)
is AsyncAction.Success -> Unit
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.features.logout.impl.R
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LogoutConfirmationDialog(
onSubmitClick: () -> Unit,
onDismiss: () -> Unit,
) {
ConfirmationDialog(
title = stringResource(id = CommonStrings.action_signout),
content = stringResource(id = R.string.screen_signout_confirmation_dialog_content),
submitText = stringResource(id = CommonStrings.action_signout),
onSubmitClick = onSubmitClick,
onDismiss = onDismiss,
)
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Вы ўпэўнены, што хочаце выйсці?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Выйсці"</string>
<string name="screen_signout_confirmation_dialog_title">"Выйсці"</string>
<string name="screen_signout_in_progress_dialog_content">"Выхад…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў."</string>
<string name="screen_signout_key_backup_disabled_title">"Вы адключылі рэзервовае капіраванне"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Вашы ключы ўсё яшчэ захоўваліся, калі вы выйшлі з сеткі. Паўторна падключыцеся, каб можна было стварыць рэзервовую копію вашых ключоў перад выхадам."</string>
<string name="screen_signout_key_backup_offline_title">"Вашы ключы ўсё яшчэ ствараюцца"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Калі ласка, дачакайцеся завяршэння працэсу, перш чым выходзіць з сістэмы."</string>
<string name="screen_signout_key_backup_ongoing_title">"Вашы ключы ўсё яшчэ ствараюцца"</string>
<string name="screen_signout_preference_item">"Выйсці"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў."</string>
<string name="screen_signout_recovery_disabled_title">"Аднаўленне не наладжана"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў."</string>
<string name="screen_signout_save_recovery_key_title">"Вы захавалі свой ключ аднаўлення?"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Сигурни ли сте, че искате да излезете?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Изход"</string>
<string name="screen_signout_confirmation_dialog_title">"Изход"</string>
<string name="screen_signout_in_progress_dialog_content">"Излизане…"</string>
<string name="screen_signout_preference_item">"Изход"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Opravdu se chcete odhlásit?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Odhlásit se"</string>
<string name="screen_signout_confirmation_dialog_title">"Odhlásit se"</string>
<string name="screen_signout_in_progress_dialog_content">"Odhlašování…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám."</string>
<string name="screen_signout_key_backup_disabled_title">"Vypnuli jste zálohování"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Když jste přešli do režimu offline, vaše klíče se ještě stále zálohovaly. Znovu se připojte, aby bylo možné před odhlášením zálohovat vaše klíče."</string>
<string name="screen_signout_key_backup_offline_title">"Vaše klíče jsou stále zálohovány"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Před odhlášením prosím počkejte na dokončení."</string>
<string name="screen_signout_key_backup_ongoing_title">"Vaše klíče jsou stále zálohovány"</string>
<string name="screen_signout_preference_item">"Odhlásit se"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám."</string>
<string name="screen_signout_recovery_disabled_title">"Obnovení není nastaveno"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, můžete ztratit přístup k šifrovaným zprávám."</string>
<string name="screen_signout_save_recovery_key_title">"Uložili jste si klíč pro obnovení?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Ydych chi\'n siŵr eich bod am allgofnodi?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Allgofnodi"</string>
<string name="screen_signout_confirmation_dialog_title">"Allgofnodi"</string>
<string name="screen_signout_in_progress_dialog_content">"Yn allgofnodi…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Rydych chi ar fin allgofnodi o\'ch sesiwn ddiwethaf. Os byddwch yn allgofnodi nawr, byddwch yn colli mynediad i\'ch negeseuon wedi\'u hamgryptio."</string>
<string name="screen_signout_key_backup_disabled_title">"Rydych chi wedi diffodd copïo wrth gefn"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Roedd eich allweddi yn dal i gael eu copïo wrth gefn pan aethoch all-lein. Ailgysylltwch fel bod modd gwneud copi wrth gefn o\'ch allweddi cyn allgofnodi."</string>
<string name="screen_signout_key_backup_offline_title">"Mae eich allweddi yn dal i gael eu copïo wrth gefn"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Arhoswch iddo gael ei gwblhau cyn allgofnodi."</string>
<string name="screen_signout_key_backup_ongoing_title">"Mae eich allweddi yn dal i gael eu copïo wrth gefn"</string>
<string name="screen_signout_preference_item">"Allgofnodi"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Rydych chi ar fin allgofnodi o\'ch sesiwn ddiwethaf. Os byddwch yn allgofnodi nawr, byddwch yn colli mynediad i\'ch negeseuon wedi\'u hamgryptio."</string>
<string name="screen_signout_recovery_disabled_title">"Adfer heb ei osod"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Rydych chi ar fin allgofnodi o\'ch sesiwn ddiwethaf. Os ydych chi\'n allgofnodi nawr, efallai y byddwch chi\'n colli mynediad i\'ch negeseuon wedi\'u hamgryptio."</string>
<string name="screen_signout_save_recovery_key_title">"Ydych chi wedi cadw\'ch allwedd adfer?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Er du sikker på, at du vil logge ud?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Log ud"</string>
<string name="screen_signout_confirmation_dialog_title">"Log ud"</string>
<string name="screen_signout_in_progress_dialog_content">"Logger ud…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Du er ved at logge ud af din sidste session. Hvis du logger ud nu, mister du adgangen til dine krypterede meddelelser."</string>
<string name="screen_signout_key_backup_disabled_title">"Du har slået sikkerhedskopiering fra"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Dine nøgler blev stadig sikkerhedskopieret, da du gik offline. Opret forbindelse igen, så dine nøgler kan sikkerhedskopieres, før du logger ud."</string>
<string name="screen_signout_key_backup_offline_title">"Dine nøgler bliver stadig sikkerhedskopieret"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Vent på, at dette er fuldført, før du logger ud."</string>
<string name="screen_signout_key_backup_ongoing_title">"Dine nøgler bliver stadig sikkerhedskopieret"</string>
<string name="screen_signout_preference_item">"Log ud"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Du er ved at logge ud af din sidste session. Hvis du logger af nu, mister du adgangen til dine krypterede meddelelser."</string>
<string name="screen_signout_recovery_disabled_title">"Gendannelse er ikke konfigureret"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Du er ved at logge ud af din sidste session. Hvis du logger af nu, kan du miste adgangen til dine krypterede meddelelser."</string>
<string name="screen_signout_save_recovery_key_title">"Har du gemt din gendannelsesnøgle?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Möchtest du dich wirklich abmelden?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Abmelden"</string>
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string>
<string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten."</string>
<string name="screen_signout_key_backup_disabled_title">"Du hast das Backup deaktiviert"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Das Backup deiner Schlüssel lief noch, als du offline gegangen bist. Verbinde dich erneut, damit deine Schlüssel vor dem Abmelden gesichert werden können."</string>
<string name="screen_signout_key_backup_offline_title">"Deine Schlüssel werden noch gesichert"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Bitte warte, bis dieser Vorgang abgeschlossen ist, bevor du dich abmeldest."</string>
<string name="screen_signout_key_backup_ongoing_title">"Deine Schlüssel werden noch gesichert"</string>
<string name="screen_signout_preference_item">"Abmelden"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten."</string>
<string name="screen_signout_recovery_disabled_title">"Wiederherstellung nicht eingerichtet"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du möglicherweise den Zugriff auf deine verschlüsselten Nachrichten."</string>
<string name="screen_signout_save_recovery_key_title">"Hast du deinen Wiederherstellungsschlüssel gespeichert?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Σίγουρα θες να αποσυνδεθείς;"</string>
<string name="screen_signout_confirmation_dialog_submit">"Αποσύνδεση"</string>
<string name="screen_signout_confirmation_dialog_title">"Αποσύνδεση"</string>
<string name="screen_signout_in_progress_dialog_content">"Αποσύνδεση…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Πρόκειται να αποσυνδεθείς από την τελευταία σου συνεδρία. Εάν αποσυνδεθείς τώρα, θα χάσεις την πρόσβαση στα κρυπτογραφημένα μηνύματά σου."</string>
<string name="screen_signout_key_backup_disabled_title">"Έχεις απενεργοποιήσει τη δημιουργία αντιγράφων ασφαλείας"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Εξακολουθούσε να δημιουργείται αντίγραφο ασφαλείας των κλειδιών σου όταν βρέθηκες εκτός σύνδεσης. Επανασυνδέσου, ώστε να είναι δυνατή η δημιουργία αντιγράφων ασφαλείας των κλειδιών σου πριν αποσυνδεθείς."</string>
<string name="screen_signout_key_backup_offline_title">"Εξακολουθούν να δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σου"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Περίμενε να ολοκληρωθεί πριν αποσυνδεθείς."</string>
<string name="screen_signout_key_backup_ongoing_title">"Εξακολουθούν να δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σου"</string>
<string name="screen_signout_preference_item">"Αποσύνδεση"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Πρόκειται να αποσυνδεθείς από την τελευταία σου συνεδρία. Εάν αποσυνδεθείς τώρα, θα χάσεις την πρόσβαση στα κρυπτογραφημένα μηνύματά σου."</string>
<string name="screen_signout_recovery_disabled_title">"Η ανάκτηση δεν έχει ρυθμιστεί"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Πρόκειται να αποσυνδεθείς από την τελευταία σας συνεδρία. Εάν αποσυνδεθείς τώρα, ενδέχεται να χάσεις την πρόσβαση στα κρυπτογραφημένα μηνύματά σου."</string>
<string name="screen_signout_save_recovery_key_title">"Έχεις αποθηκεύσει το κλειδί ανάκτησης;"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"¿Estás seguro de que quieres cerrar sesión?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Cerrar sesión"</string>
<string name="screen_signout_confirmation_dialog_title">"Cerrar sesión"</string>
<string name="screen_signout_in_progress_dialog_content">"Cerrando sesión…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Estás a punto de cerrar tu última sesión. Si cierras sesión ahora, perderás el acceso a tus mensajes cifrados."</string>
<string name="screen_signout_key_backup_disabled_title">"Has desactivado la copia de seguridad"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Se estaba haciendo una copia de seguridad de tus claves cuando te desconectaste. Vuelve a conectarte para que se haga una copia de seguridad de tus claves antes de desconectarte."</string>
<string name="screen_signout_key_backup_offline_title">"Se sigue guardando una copia de seguridad de tus claves"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Espera a que se complete antes de cerrar sesión."</string>
<string name="screen_signout_key_backup_ongoing_title">"Se sigue guardando una copia de seguridad de tus claves"</string>
<string name="screen_signout_preference_item">"Cerrar sesión"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Estás a punto de cerrar tu última sesión. Si cierras sesión ahora, perderás el acceso a tus mensajes cifrados."</string>
<string name="screen_signout_recovery_disabled_title">"La recuperación no está configurada"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Estás a punto de cerrar tu última sesión. Si cierras la sesión ahora, podrías perder el acceso a tus mensajes cifrados."</string>
<string name="screen_signout_save_recovery_key_title">"¿Has guardado tu clave de recuperación?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Kas sa oled kindel, et soovid välja logida?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Logi välja"</string>
<string name="screen_signout_confirmation_dialog_title">"Logi välja"</string>
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Oled oma viimasest seansist välja logimas. Kui logid nüüd välja, kaotad ligipääsu oma krüptitud sõnumitele."</string>
<string name="screen_signout_key_backup_disabled_title">"Sa oled varukoopiate tegemise välja lülitanud"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Kui su võrguühendus katkes, siis sinu krüptovõtmed oli parasjagu varundamisel. Loo võrguühendus uuesti, oota kuni krüptovõtmete varundamine lõppeb ja alles siis logi rakendusest välja."</string>
<string name="screen_signout_key_backup_offline_title">"Sinu krüptovõtmed on veel varundamisel"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Enne väljalogimist palun oota, et pooleliolev toiming lõppeb."</string>
<string name="screen_signout_key_backup_ongoing_title">"Sinu krüptovõtmed on veel varundamisel"</string>
<string name="screen_signout_preference_item">"Logi välja"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis kaotad ligipääsu oma krüptitud sõnumitele."</string>
<string name="screen_signout_recovery_disabled_title">"Andmete taastamine on seadistamata"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis ilmselt kaotad ligipääsu oma krüptitud sõnumitele."</string>
<string name="screen_signout_save_recovery_key_title">"Kas sa oled oma taastevõtme talletanud?"</string>
</resources>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Ziur saioa amaitu nahi duzula?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Amaitu saioa"</string>
<string name="screen_signout_confirmation_dialog_title">"Amaitu saioa"</string>
<string name="screen_signout_in_progress_dialog_content">"Saioa amaitzen…"</string>
<string name="screen_signout_key_backup_disabled_title">"Babeskopia desaktibatu duzu"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Itxaron eragiketa amaitu arte saioa amaitu baino lehen."</string>
<string name="screen_signout_preference_item">"Amaitu saioa"</string>
<string name="screen_signout_recovery_disabled_title">"Berreskuratzea ez da konfiguratu"</string>
<string name="screen_signout_save_recovery_key_title">"Gorde al duzu berreskuratze-gakoa?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"مطمئنید که می‌خواهید از حسابتان خارج شوید؟"</string>
<string name="screen_signout_confirmation_dialog_submit">"خروج"</string>
<string name="screen_signout_confirmation_dialog_title">"خروج"</string>
<string name="screen_signout_in_progress_dialog_content">"خارج شدن…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"دارید از واپسین نشستتان خارج می‌شوید. اگر اکنون خارج شوید پیام‌های رمزنگاشته‌تان را از دست خواهید داد."</string>
<string name="screen_signout_key_backup_disabled_title">"پشتیبان را خاموش کرده‌اید"</string>
<string name="screen_signout_key_backup_offline_subtitle">"در هنگامی که آفلاین شدید، کلیدهای شما هنوز در حال پشتیبان‌گیری بودند. دوباره متصل شوید ، تا قبل از خروج از کلیدهایتان نسخه پشتیبان‌ گرفته شود."</string>
<string name="screen_signout_key_backup_offline_title">"کلیدهایتان هنوز در حال پشتیبان گیریند"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"لطفاً پیش از خروج منتظر پایانش شوید."</string>
<string name="screen_signout_key_backup_ongoing_title">"کلیدهایتان هنوز در حال پشتیبان گیریند"</string>
<string name="screen_signout_preference_item">"خروج"</string>
<string name="screen_signout_recovery_disabled_subtitle">"شما در آستانه خروج از آخرین جلسه خود هستید. اگر اکنون از سیستم خارج شوید، دسترسی به پیام های رمزگذاری شده تان را از دست خواهید داد."</string>
<string name="screen_signout_recovery_disabled_title">"بازگردانی برپا نشده"</string>
<string name="screen_signout_save_recovery_key_subtitle">"دارید از واپسین نشستتان خارج می‌شوید. اگر اکنون خارج شوید ممکن است پیام‌های رمزنگاشته‌تان را از دست بدهید."</string>
<string name="screen_signout_save_recovery_key_title">"کلید بازیابیتان را ذخیره کرده‌اید؟"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Haluatko varmasti kirjautua ulos?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Kirjaudu ulos"</string>
<string name="screen_signout_confirmation_dialog_title">"Kirjaudu ulos"</string>
<string name="screen_signout_in_progress_dialog_content">"Kirjaudutaan ulos…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Olet kirjautumassa ulos viimeisestä istunnostasi. Jos kirjaudut ulos nyt, menetät pääsyn salattuihin viesteihisi."</string>
<string name="screen_signout_key_backup_disabled_title">"Olet poistanut varmuuskopioinnin käytöstä"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Avaimiasi varmuuskopioitiin vielä, kun menit offline-tilaan. Muodosta yhteys uudelleen, jotta avaimesi voidaan varmuuskopioida ennen uloskirjautumista."</string>
<string name="screen_signout_key_backup_offline_title">"Avaimiasi varmuuskopioidaan vielä"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Odota, että tämä on valmis ennen uloskirjautumista."</string>
<string name="screen_signout_key_backup_ongoing_title">"Avaimiasi varmuuskopioidaan vielä"</string>
<string name="screen_signout_preference_item">"Kirjaudu ulos"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Olet kirjautumassa ulos viimeisestä istunnostasi. Jos kirjaudut ulos nyt, menetät pääsyn salattuihin viesteihisi."</string>
<string name="screen_signout_recovery_disabled_title">"Palautus ei ole käytössä"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Olet kirjautumassa ulos viimeisestä istunnostasi. Jos kirjaudut ulos nyt, saatat menettää pääsyn salattuihin viesteihisi."</string>
<string name="screen_signout_save_recovery_key_title">"Oletko tallentanut palautusavaimesi?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Êtes-vous sûr de vouloir vous déconnecter ?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Se déconnecter"</string>
<string name="screen_signout_confirmation_dialog_title">"Se déconnecter"</string>
<string name="screen_signout_in_progress_dialog_content">"Déconnexion…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Vous êtes en train de vous déconnecter de votre dernière session. Si vous vous déconnectez maintenant, vous perdrez laccès à lhistorique de vos discussions chiffrées."</string>
<string name="screen_signout_key_backup_disabled_title">"Vous avez désactivé la sauvegarde"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Vos clés étaient en cours de sauvegarde lorsque vous avez perdu la connexion au réseau. Il faudrait rétablir cette connexion afin de pouvoir terminer la sauvegarde avant de vous déconnecter."</string>
<string name="screen_signout_key_backup_offline_title">"Vos clés sont en cours de sauvegarde"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Veuillez attendre que cela se termine avant de vous déconnecter."</string>
<string name="screen_signout_key_backup_ongoing_title">"Vos clés sont en cours de sauvegarde"</string>
<string name="screen_signout_preference_item">"Se déconnecter"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Vous êtes sur le point de vous déconnecter de votre dernier appareil. Si vous le faites maintenant, vous perdrez laccès à lhistorique de vos messages."</string>
<string name="screen_signout_recovery_disabled_title">"La récupération nest pas configurée."</string>
<string name="screen_signout_save_recovery_key_subtitle">"Vous êtes sur le point de vous déconnecter de votre dernière session. Si vous le faites maintenant, vous perdrez laccès à lhistorique de vos discussions chiffrées."</string>
<string name="screen_signout_save_recovery_key_title">"Avez-vous sauvegardé votre clé de récupération ?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Biztos, hogy kijelentkezik?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Kijelentkezés"</string>
<string name="screen_signout_confirmation_dialog_title">"Kijelentkezés"</string>
<string name="screen_signout_in_progress_dialog_content">"Kijelentkezés…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez."</string>
<string name="screen_signout_key_backup_disabled_title">"Kikapcsolta a biztonsági mentést"</string>
<string name="screen_signout_key_backup_offline_subtitle">"A kulcsai mentése során bontotta a kapcsolatot. Kapcsolódjon újra, hogy a kulcsai továbbra is mentésre kerüljenek mielőtt kijelentkezik."</string>
<string name="screen_signout_key_backup_offline_title">"A kulcsai mentése még folyamatban van"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Kijelentkezés előtt várja meg a befejezését."</string>
<string name="screen_signout_key_backup_ongoing_title">"A kulcsai mentése még folyamatban van"</string>
<string name="screen_signout_preference_item">"Kijelentkezés"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez."</string>
<string name="screen_signout_recovery_disabled_title">"A helyreállítás nincs beállítva"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszítheti a hozzáférését a titkosított üzeneteihez."</string>
<string name="screen_signout_save_recovery_key_title">"Mentette a helyreállítási kulcsát?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Apakah Anda yakin ingin keluar dari akun?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Keluar dari akun"</string>
<string name="screen_signout_confirmation_dialog_title">"Keluar dari akun"</string>
<string name="screen_signout_in_progress_dialog_content">"Mengeluarkan dari akun…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Anda akan keluar dari sesi terakhir Anda. Jika Anda keluar sekarang, Anda akan kehilangan akses ke pesan terenkripsi Anda."</string>
<string name="screen_signout_key_backup_disabled_title">"Anda telah menonaktifkan pencadangan"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Kunci Anda masih dicadangkan saat Anda luring. Sambungkan kembali sehingga kunci Anda dapat dicadangkan sebelum keluar."</string>
<string name="screen_signout_key_backup_offline_title">"Kunci Anda masih dicadangkan"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Mohon tunggu hingga proses ini selesai sebelum keluar."</string>
<string name="screen_signout_key_backup_ongoing_title">"Kunci Anda masih dicadangkan"</string>
<string name="screen_signout_preference_item">"Keluar dari akun"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Anda akan keluar dari sesi Anda yang terakhir. Jika Anda keluar sekarang, Anda akan kehilangan akses ke pesan terenkripsi Anda."</string>
<string name="screen_signout_recovery_disabled_title">"Pemulihan belum disiapkan"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Anda akan keluar dari sesi terakhir Anda. Jika Anda keluar sekarang, Anda mungkin kehilangan akses ke pesan terenkripsi Anda."</string>
<string name="screen_signout_save_recovery_key_title">"Apakah Anda sudah menyimpan kunci pemulihan Anda?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Sei sicuro di voler uscire?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Disconnetti"</string>
<string name="screen_signout_confirmation_dialog_title">"Disconnetti"</string>
<string name="screen_signout_in_progress_dialog_content">"Disconnessione in corso…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati."</string>
<string name="screen_signout_key_backup_disabled_title">"Hai disattivato il backup"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Il backup delle chiavi era ancora in corso quando sei andato offline. Riconnettiti per eseguire il backup delle chiavi prima di uscire."</string>
<string name="screen_signout_key_backup_offline_title">"Il backup delle chiavi è ancora in corso"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Attendi il completamento dell\'operazione prima di uscire."</string>
<string name="screen_signout_key_backup_ongoing_title">"Il backup delle chiavi è ancora in corso"</string>
<string name="screen_signout_preference_item">"Disconnetti"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati."</string>
<string name="screen_signout_recovery_disabled_title">"Recupero non impostato"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Stai per disconnettere la tua ultima sessione. Se esci ora, potresti perdere l\'accesso ai tuoi messaggi cifrati."</string>
<string name="screen_signout_save_recovery_key_title">"Hai salvato la chiave di recupero?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"დარწმუნებული ხართ, რომ გსურთ გამოსვლა?"</string>
<string name="screen_signout_confirmation_dialog_submit">"გამოსვლა"</string>
<string name="screen_signout_confirmation_dialog_title">"გამოსვლა"</string>
<string name="screen_signout_in_progress_dialog_content">"გასვლა…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"თქვენ აპირებთ გასვლას თქვენი ბოლო სესიიდან. თუ ახლა გამოხვალთ, დაკარგავთ წვდომას თქვენს დაშიფრულ შეტყობინებებზე."</string>
<string name="screen_signout_key_backup_disabled_title">"თქვენ გამორთეთ სარეზერვო ასლი"</string>
<string name="screen_signout_key_backup_offline_subtitle">"თქვენი გასაღებების სარეზერვო ასლის შექმნა მიმდინარეობდა იმ დროს, როდესაც გამოხვედით. დაკავშირდით ისევ ისე, რომ სარეზერვო ასლი შეიქმნას ანგარიშიდან გამოსვლის გარეშე."</string>
<string name="screen_signout_key_backup_offline_title">"თქვენი გასაღებების სარეზერვო ასლი ჯერ კიდევ შექმნის პროცესშია"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"გთხოვთ დაელოდეთ ამის დასრულებას სისტემიდან გამოსვლამდე."</string>
<string name="screen_signout_key_backup_ongoing_title">"თქვენი გასაღებების სარეზერვო ასლი ჯერ კიდევ შექმნის პროცესშია"</string>
<string name="screen_signout_preference_item">"გამოსვლა"</string>
<string name="screen_signout_recovery_disabled_subtitle">"თქვენ აპირებთ გასვლას თქვენი ბოლო სესიიდან. თუ ახლა გამოხვალთ, დაკარგავთ წვდომას თქვენს დაშიფრულ შეტყობინებებზე."</string>
<string name="screen_signout_recovery_disabled_title">"აღდგენა არ არის დაყენებული"</string>
<string name="screen_signout_save_recovery_key_subtitle">"თქვენ აპირებთ გასვლას თქვენი ბოლო სესიიდან. თუ ახლა გამოხვალთ, შესაძლოა დაკარგოთ წვდომა თქვენს დაშიფრულ შეტყობინებებზე."</string>
<string name="screen_signout_save_recovery_key_title">"შეინახეთ თქვენი აღდგენის გასაღები?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"정말 로그아웃하시겠습니까?"</string>
<string name="screen_signout_confirmation_dialog_submit">"로그아웃"</string>
<string name="screen_signout_confirmation_dialog_title">"로그아웃"</string>
<string name="screen_signout_in_progress_dialog_content">"로그아웃 중…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"마지막 세션에서 로그아웃하려고 합니다. 지금 로그아웃하면 암호화된 메시지에 액세스할 수 없게 됩니다."</string>
<string name="screen_signout_key_backup_disabled_title">"백업이 꺼져 있습니다."</string>
<string name="screen_signout_key_backup_offline_subtitle">"오프라인으로 전환했을 때 키가 아직 백업 중이었습니다. 로그아웃하기 전에 키를 백업할 수 있도록 다시 연결하세요."</string>
<string name="screen_signout_key_backup_offline_title">"귀하의 키는 아직 백업 중입니다."</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"로그아웃하기 전에 이 과정이 완료될 때까지 기다려 주시기 바랍니다."</string>
<string name="screen_signout_key_backup_ongoing_title">"귀하의 키는 아직 백업 중입니다."</string>
<string name="screen_signout_preference_item">"로그아웃"</string>
<string name="screen_signout_recovery_disabled_subtitle">"마지막 세션에서 로그아웃할 것입니다. 지금 로그아웃하면 암호화된 메시지에 액세스할 수 없게 됩니다."</string>
<string name="screen_signout_recovery_disabled_title">"복구가 설정되지 않았습니다"</string>
<string name="screen_signout_save_recovery_key_subtitle">"마지막 세션에서 로그아웃하려고 합니다. 지금 로그아웃하면 암호화된 메시지에 액세스할 수 없게 될 수 있습니다."</string>
<string name="screen_signout_save_recovery_key_title">"복구 키를 저장하셨습니까?"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Ar tikrai norite atsijungti?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Atsijungti"</string>
<string name="screen_signout_confirmation_dialog_title">"Atsijungti"</string>
<string name="screen_signout_in_progress_dialog_content">"Atsijungiama…"</string>
<string name="screen_signout_preference_item">"Atsijungti"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Er du sikker på at du vil logge ut?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Logg ut"</string>
<string name="screen_signout_confirmation_dialog_title">"Logg ut"</string>
<string name="screen_signout_in_progress_dialog_content">"Logger ut…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Du er i ferd med å logge av din siste sesjon. Hvis du logger av nå, mister du tilgangen til de krypterte meldingene dine."</string>
<string name="screen_signout_key_backup_disabled_title">"Du har slått av sikkerhetskopiering"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Nøklene dine ble fortsatt sikkerhetskopiert da du koblet fra. Koble til igjen, slik at nøklene dine kan sikkerhetskopieres før du logger av."</string>
<string name="screen_signout_key_backup_offline_title">"Nøklene dine blir fortsatt sikkerhetskopiert"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Vent til dette er fullført før du logger av."</string>
<string name="screen_signout_key_backup_ongoing_title">"Nøklene dine blir fortsatt sikkerhetskopiert"</string>
<string name="screen_signout_preference_item">"Logg ut"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Du er i ferd med å logge ut av din siste sesjon. Hvis du logger ut nå, mister du tilgangen til de krypterte meldingene dine."</string>
<string name="screen_signout_recovery_disabled_title">"Gjenoppretting ikke konfigurert"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Du er i ferd med å logge av din siste sesjon. Hvis du logger av nå, kan du miste tilgangen til de krypterte meldingene dine."</string>
<string name="screen_signout_save_recovery_key_title">"Har du lagret gjenopprettingsnøkkelen din?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Weet je zeker dat je je wilt uitloggen?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Uitloggen"</string>
<string name="screen_signout_confirmation_dialog_title">"Uitloggen"</string>
<string name="screen_signout_in_progress_dialog_content">"Uitloggen…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Je staat op het punt uit te loggen bij je laatste sessie. Als je je nu uitlogt, verlies je de toegang tot je versleutelde berichten."</string>
<string name="screen_signout_key_backup_disabled_title">"Je hebt de back-up uitgeschakeld"</string>
<string name="screen_signout_key_backup_offline_subtitle">"De backup van je sleutels was nog bezig toen je offline ging. Maak opnieuw verbinding zodat er een back-up van je sleutels kan worden gemaakt voordat je uitlogt."</string>
<string name="screen_signout_key_backup_offline_title">"De backup van je sleutels is nog bezig"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Wacht tot dit voltooid is voordat je uitlogt."</string>
<string name="screen_signout_key_backup_ongoing_title">"De backup van je sleutels is nog bezig"</string>
<string name="screen_signout_preference_item">"Uitloggen"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Je staat op het punt uit te loggen bij je laatste sessie. Als je je nu uitlogt, verlies je de toegang tot je versleutelde berichten."</string>
<string name="screen_signout_recovery_disabled_title">"Herstelmogelijkheid niet ingesteld"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Je staat op het punt uit te loggen bij je laatste sessie. Als je je nu uitlogt, kan het dat je de toegang tot je versleutelde berichten verliest."</string>
<string name="screen_signout_save_recovery_key_title">"Heb je je herstelsleutel opgeslagen?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Czy na pewno chcesz się wylogować?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Wyloguj"</string>
<string name="screen_signout_confirmation_dialog_title">"Wyloguj"</string>
<string name="screen_signout_in_progress_dialog_content">"Wylogowywanie…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string>
<string name="screen_signout_key_backup_disabled_title">"Wyłączyłeś backup"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Twoje klucze były nadal archiwizowane po przejściu w tryb offline. Połącz się ponownie, aby zapisać w chmurze przed wylogowaniem."</string>
<string name="screen_signout_key_backup_offline_title">"Twoje klucze są nadal archiwizowane"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Zanim się wylogujesz, poczekaj na zakończenie operacji."</string>
<string name="screen_signout_key_backup_ongoing_title">"Twoje klucze są nadal archiwizowane"</string>
<string name="screen_signout_preference_item">"Wyloguj"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string>
<string name="screen_signout_recovery_disabled_title">"Nie ustawiono przywracania"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string>
<string name="screen_signout_save_recovery_key_title">"Czy zapisałeś swój klucz przywracania?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Você tem certeza que deseja sair?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Sair"</string>
<string name="screen_signout_confirmation_dialog_title">"Sair"</string>
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Você está prestes a sair da sua última sessão. Se você sair agora, perderá o acesso às suas mensagens criptografadas."</string>
<string name="screen_signout_key_backup_disabled_title">"Você desativou o backup"</string>
<string name="screen_signout_key_backup_offline_subtitle">"O backup das suas chaves ainda estava sendo feito quando você ficou off-line. Reconecte-se para que você possa fazer o backup de suas chaves antes de sair."</string>
<string name="screen_signout_key_backup_offline_title">"O backup das suas chaves ainda está em andamento"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Aguarde até que isso seja concluído antes de sair."</string>
<string name="screen_signout_key_backup_ongoing_title">"O backup das suas chaves ainda está em andamento"</string>
<string name="screen_signout_preference_item">"Sair"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Você está prestes a sair da sua última sessão. Se você sair agora, perderá o acesso às suas mensagens criptografadas."</string>
<string name="screen_signout_recovery_disabled_title">"A recuperação não está configurada"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Você está prestes a sair da sua última sessão. Se você sair agora, poderá perder o acesso às suas mensagens criptografadas."</string>
<string name="screen_signout_save_recovery_key_title">"Você salvou sua chave de recuperação?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Tens a certeza que queres terminar a sessão?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Terminar sessão"</string>
<string name="screen_signout_confirmation_dialog_title">"Terminar sessão"</string>
<string name="screen_signout_in_progress_dialog_content">"A terminar sessão…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Estás prestes a terminar a tua última sessão. Se continuares, perderás o acesso às tuas mensagens cifradas."</string>
<string name="screen_signout_key_backup_disabled_title">"Desativaste a cópia de segurança"</string>
<string name="screen_signout_key_backup_offline_subtitle">"As tuas chaves ainda estavam a ser guardadas quando ficaste desligado. Volta a ligar-te para que as tuas chaves possam ser guardadas antes de encerrares a sessão."</string>
<string name="screen_signout_key_backup_offline_title">"As tuas chaves ainda estão a ser guardadas"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Por favor, aguarda a conclusão desta operação antes de terminares a sessão."</string>
<string name="screen_signout_key_backup_ongoing_title">"As tuas chaves ainda estão a ser guardadas"</string>
<string name="screen_signout_preference_item">"Terminar sessão"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Estás prestes a terminar a tua última sessão. Se continuares, perderás o acesso às tuas mensagens cifradas."</string>
<string name="screen_signout_recovery_disabled_title">"Recuperação não configurada"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Estás prestes a terminar a tua última sessão. Se continuares, poderás perder o acesso às tuas mensagens cifradas."</string>
<string name="screen_signout_save_recovery_key_title">"Guardaste a tua chave de recuperação?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Sunteți sigur că vreți să vă deconectați?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Deconectați-vă"</string>
<string name="screen_signout_confirmation_dialog_title">"Deconectați-vă"</string>
<string name="screen_signout_in_progress_dialog_content">"Deconectare în curs…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, veți pierde accesul la mesajele criptate."</string>
<string name="screen_signout_key_backup_disabled_title">"Ați dezactivat backup-ul"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Cheile dumneavoastră erau încă în curs de backup atunci când ați fost deconectat. Reconectați-vă pentru ca cheile dumneavoastră să poată fi salvate înainte de a vă deconecta."</string>
<string name="screen_signout_key_backup_offline_title">"Cheile dumneavoastră sunt încă în curs de backup"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Vă rugăm să așteptați până la finalizarea acestui proces înainte de a vă deconecta."</string>
<string name="screen_signout_key_backup_ongoing_title">"Cheile dumneavoastră sunt încă în curs de backup"</string>
<string name="screen_signout_preference_item">"Deconectați-vă"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, veți pierde accesul la mesajele criptate."</string>
<string name="screen_signout_recovery_disabled_title">"Recuperarea nu este configurată"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, este posibil să pierdeți accesul la mesajele criptate."</string>
<string name="screen_signout_save_recovery_key_title">"Ați salvat cheia de recuperare?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Вы уверены, что вы хотите выйти?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Выйти"</string>
<string name="screen_signout_confirmation_dialog_title">"Выйти"</string>
<string name="screen_signout_in_progress_dialog_content">"Выполняется выход…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям."</string>
<string name="screen_signout_key_backup_disabled_title">"Вы отключили резервное копирование"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Когда вы перешли в автономный режим, резервное копирование ваших ключей продолжалось. Повторно подключитесь, чтобы перед выходом из системы можно было создать резервную копию ключей."</string>
<string name="screen_signout_key_backup_offline_title">"Резервное копирование ключей все еще продолжается"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Пожалуйста, дождитесь завершения процесса, прежде чем выходить из системы."</string>
<string name="screen_signout_key_backup_ongoing_title">"Резервное копирование ключей все еще продолжается"</string>
<string name="screen_signout_preference_item">"Выйти"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям."</string>
<string name="screen_signout_recovery_disabled_title">"Восстановление не настроено"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы можете потерять доступ к зашифрованным сообщениям."</string>
<string name="screen_signout_save_recovery_key_title">"Вы сохранили ключ восстановления?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Ste si istí, že sa chcete odhlásiť?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Odhlásiť sa"</string>
<string name="screen_signout_confirmation_dialog_title">"Odhlásiť sa"</string>
<string name="screen_signout_in_progress_dialog_content">"Prebieha odhlasovanie…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam."</string>
<string name="screen_signout_key_backup_disabled_title">"Vypli ste zálohovanie"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Keď ste sa odpojili od internetu, vaše kľúče sa ešte stále zálohovali. Pripojte sa znova k internetu, aby sa vaše kľúče mohli zálohovať pred odhlásením."</string>
<string name="screen_signout_key_backup_offline_title">"Vaše kľúče sa ešte stále zálohujú"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Pred odhlásením počkajte, kým sa to dokončí."</string>
<string name="screen_signout_key_backup_ongoing_title">"Vaše kľúče sa ešte stále zálohujú"</string>
<string name="screen_signout_preference_item">"Odhlásiť sa"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam."</string>
<string name="screen_signout_recovery_disabled_title">"Obnovenie nie je nastavené"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam."</string>
<string name="screen_signout_save_recovery_key_title">"Uložili ste kľúč na obnovenie?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Är du säker på att du vill logga ut?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Logga ut"</string>
<string name="screen_signout_confirmation_dialog_title">"Logga ut"</string>
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Du är på väg att logga ut ur din senaste session. Om du loggar ut nu kommer du att förlora åtkomsten till dina krypterade meddelanden."</string>
<string name="screen_signout_key_backup_disabled_title">"Du har stängt av säkerhetskopiering"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Dina nycklar säkerhetskopierades fortfarande när du gick offline. Anslut igen så att dina nycklar kan säkerhetskopieras innan du loggar ut."</string>
<string name="screen_signout_key_backup_offline_title">"Dina nycklar säkerhetskopieras fortfarande"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Vänta tills detta är klart innan du loggar ut."</string>
<string name="screen_signout_key_backup_ongoing_title">"Dina nycklar säkerhetskopieras fortfarande"</string>
<string name="screen_signout_preference_item">"Logga ut"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Du är på väg att logga ut ur din sista session. Om du loggar ut nu förlorar du åtkomsten till dina krypterade meddelanden."</string>
<string name="screen_signout_recovery_disabled_title">"Återställning inte inställd"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Du är på väg att logga ut från din senaste session. Om du loggar ut nu kan du förlora åtkomsten till dina krypterade meddelanden."</string>
<string name="screen_signout_save_recovery_key_title">"Har du sparat din återställningsnyckel?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Çıkış yapmak istediğinize emin misiniz?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Oturumu kapat"</string>
<string name="screen_signout_confirmation_dialog_title">"Oturumu kapat"</string>
<string name="screen_signout_in_progress_dialog_content">"Oturum kapatılıyor…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Son oturumunuzdan çıkmak üzeresiniz. Şimdi çıkış yaparsanız, şifrelenmiş mesajlarınıza erişiminizi kaybedersiniz."</string>
<string name="screen_signout_key_backup_disabled_title">"Yedeklemeyi kapattınız"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Çevrimdışı olduğunuzda anahtarlarınız hala yedekleniyordu. Oturumu kapatmadan önce anahtarlarınızın yedeklenebilmesi için yeniden bağlanın."</string>
<string name="screen_signout_key_backup_offline_title">"Anahtarlarınız hala yedekleniyor"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Lütfen oturumu kapatmadan önce bunun tamamlanmasını bekleyin."</string>
<string name="screen_signout_key_backup_ongoing_title">"Anahtarlarınız hala yedekleniyor"</string>
<string name="screen_signout_preference_item">"Oturumu kapat"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Son oturumunuzdan çıkmak üzeresiniz. Şimdi çıkış yaparsanız şifrelenmiş mesajlarınıza erişiminizi kaybedersiniz."</string>
<string name="screen_signout_recovery_disabled_title">"Kurtarma ayarlanmadı"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Son oturumunuzdan çıkmak üzeresiniz. Şimdi oturumu kapatırsanız şifrelenmiş mesajlarınıza erişiminizi kaybedebilirsiniz."</string>
<string name="screen_signout_save_recovery_key_title">"Kurtarma anahtarınızı kaydettiniz mi?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Ви впевнені, що бажаєте вийти?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Вийти"</string>
<string name="screen_signout_confirmation_dialog_title">"Вийти"</string>
<string name="screen_signout_in_progress_dialog_content">"Вихід…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень."</string>
<string name="screen_signout_key_backup_disabled_title">"Ви вимкнули резервне копіювання"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Коли ви вийшли з мережі, резервна копія ваших ключів все ще створювалася. Повторно під\'єднайтеся, щоб зберегти резервну копію ключів перед виходом."</string>
<string name="screen_signout_key_backup_offline_title">"Резервне копіювання ваших ключів ще триває"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Дочекайтеся завершення процесу, перш ніж вийти."</string>
<string name="screen_signout_key_backup_ongoing_title">"Резервне копіювання ваших ключів ще триває"</string>
<string name="screen_signout_preference_item">"Вийти"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень."</string>
<string name="screen_signout_recovery_disabled_title">"Відновлення не налаштовано"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Ви збираєтеся вийти зі свого останнього сеансу. Якщо вийти зараз, ви можете втратити доступ до зашифрованих повідомлень."</string>
<string name="screen_signout_save_recovery_key_title">"Ви зберегли ключ відновлення?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"کیا آپ واقعی خارج ہونا چاہتے ہیں؟"</string>
<string name="screen_signout_confirmation_dialog_submit">"خارج ہوں"</string>
<string name="screen_signout_confirmation_dialog_title">"خارج ہوں"</string>
<string name="screen_signout_in_progress_dialog_content">"خارج ہورہاہے…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"آپ اپنے آخری جلسے سے خارج ہونے والے ہیں۔ اگر آپ ابھی خارج ہوجاتے ہیں تو آپ اپنے مرموز کردہ پیغامات تک رسائی سے محروم ہو جائیں گے۔"</string>
<string name="screen_signout_key_backup_disabled_title">"آپنے پشتارہ بند کردیا ہے"</string>
<string name="screen_signout_key_backup_offline_subtitle">"جب آپ پرے خط تھے تب بھی آپ کی کلیدوں کا پشتارہ کیا جا رہا تھا۔ دوبارہ جڑیں تاکہ خارج ہونے سے پہلے آپ کی کلیدوں کا پشتارہ کیا جا سکے۔"</string>
<string name="screen_signout_key_backup_offline_title">"آپکی کلیدوں کا ابھی بھی پشتارہ کیا جا رہا ہے۔"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"برائے مہربانی خارج ہونے سے پہلے اسکے مکمل ہونے کا انتظار کریں"</string>
<string name="screen_signout_key_backup_ongoing_title">"آپکی کلیدوں کا ابھی بھی پشتارہ کیا جا رہا ہے۔"</string>
<string name="screen_signout_preference_item">"خارج ہوں"</string>
<string name="screen_signout_recovery_disabled_subtitle">"آپ اپنے آخری جلسے سے خارج ہونے والے ہیں۔ اگر آپ ابھی خارج ہوجاتے ہیں تو آپ اپنے مرموز کردہ پیغامات تک رسائی سے محروم ہو جائیں گے۔"</string>
<string name="screen_signout_recovery_disabled_title">"بازیابی غیر مرتب"</string>
<string name="screen_signout_save_recovery_key_subtitle">"آپ اپنے آخری جلسے سے خارج ہونے والے ہیں۔ اگر آپ ابھی خارج ہوجاتے ہیں تو ہوسکتا ہے کہ آپ اپنے مرموز کردہ پیغامات تک رسائی سے محروم ہو جائیں گے۔"</string>
<string name="screen_signout_save_recovery_key_title">"کیا آپ نے اپنی بازیابی کی کلید محفوظ کی ہے؟"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Haqiqatan ham tizimdan chiqmoqchimisiz?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Tizimdan chiqish"</string>
<string name="screen_signout_confirmation_dialog_title">"Tizimdan chiqish"</string>
<string name="screen_signout_in_progress_dialog_content">"Chiqish…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmaysiz."</string>
<string name="screen_signout_key_backup_disabled_title">"Siz zaxira nusxasini oʻchirdingiz"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Siz oflayn bolganingizda ham kalitlaringiz zaxiralanish jarayonida edi. Tizimdan chiqishdan oldin kalitlaringizning toliq zaxiralanishini taminlash uchun qayta ulanishingiz zarur."</string>
<string name="screen_signout_key_backup_offline_title">"Kalitlaringiz hamon zaxiralanmoqda"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Tizimdan chiqishdan oldin bu jarayon tugashini kuting."</string>
<string name="screen_signout_key_backup_ongoing_title">"Kalitlaringiz hamon zaxiralanmoqda"</string>
<string name="screen_signout_preference_item">"Tizimdan chiqish"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmaysiz."</string>
<string name="screen_signout_recovery_disabled_title">"Qayta tiklash sozlanmagan"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmay qolishingiz mumkin."</string>
<string name="screen_signout_save_recovery_key_title">"Zaxira kalitingizni saqladingizmi?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"您確定要登出嗎?"</string>
<string name="screen_signout_confirmation_dialog_submit">"登出"</string>
<string name="screen_signout_confirmation_dialog_title">"登出"</string>
<string name="screen_signout_in_progress_dialog_content">"正在登出…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"您將要登出上一次作業階段。若您現在登出,將會失去對加密訊息的存取權。"</string>
<string name="screen_signout_key_backup_disabled_title">"您已關閉備份"</string>
<string name="screen_signout_key_backup_offline_subtitle">"當您離線時,您的金鑰仍在備份中。請重新連線才能在您登出前備份金鑰。"</string>
<string name="screen_signout_key_backup_offline_title">"您的金鑰仍在備份中"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"請等待此動作完成後再登出。"</string>
<string name="screen_signout_key_backup_ongoing_title">"您的金鑰仍在備份中"</string>
<string name="screen_signout_preference_item">"登出"</string>
<string name="screen_signout_recovery_disabled_subtitle">"您將要登出上一次作業階段。若您現在登出,將會失去對加密訊息的存取權。"</string>
<string name="screen_signout_recovery_disabled_title">"未設定復原金鑰"</string>
<string name="screen_signout_save_recovery_key_subtitle">"您將要登出上一次作業階段。若您現在登出,將會失去對加密訊息的存取權。"</string>
<string name="screen_signout_save_recovery_key_title">"您儲存復原金鑰了嗎?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"确定要登出吗?"</string>
<string name="screen_signout_confirmation_dialog_submit">"登出"</string>
<string name="screen_signout_confirmation_dialog_title">"登出"</string>
<string name="screen_signout_in_progress_dialog_content">"正在登出…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"即将登出最后一个会话。如果现在登出,将无法访问加密的消息。"</string>
<string name="screen_signout_key_backup_disabled_title">"您已关闭备份"</string>
<string name="screen_signout_key_backup_offline_subtitle">"当你离线时,密钥仍在备份中。重新连接以便在登出之前备份密钥。"</string>
<string name="screen_signout_key_backup_offline_title">"您的密钥仍在备份中"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"请等待此操作完成后再登出。"</string>
<string name="screen_signout_key_backup_ongoing_title">"您的密钥仍在备份中"</string>
<string name="screen_signout_preference_item">"登出"</string>
<string name="screen_signout_recovery_disabled_subtitle">"即将登出最后一个会话。如果现在登出,将无法访问加密的消息。"</string>
<string name="screen_signout_recovery_disabled_title">"未设置恢复"</string>
<string name="screen_signout_save_recovery_key_subtitle">"即将登出最后一个会话。如果现在登出,将无法访问加密的消息。"</string>
<string name="screen_signout_save_recovery_key_title">"您保存了恢复密钥吗?"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Are you sure you want to sign out?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Sign out"</string>
<string name="screen_signout_confirmation_dialog_title">"Sign out"</string>
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."</string>
<string name="screen_signout_key_backup_disabled_title">"You have turned off backup"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out."</string>
<string name="screen_signout_key_backup_offline_title">"Your keys are still being backed up"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Please wait for this to complete before signing out."</string>
<string name="screen_signout_key_backup_ongoing_title">"Your keys are still being backed up"</string>
<string name="screen_signout_preference_item">"Sign out"</string>
<string name="screen_signout_recovery_disabled_subtitle">"You are about to sign out of your last session. If you sign out now, you\'ll lose access to your encrypted messages."</string>
<string name="screen_signout_recovery_disabled_title">"Recovery not set up"</string>
<string name="screen_signout_save_recovery_key_subtitle">"You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."</string>
<string name="screen_signout_save_recovery_key_title">"Have you saved your recovery key?"</string>
</resources>
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import org.junit.Rule
import org.junit.Test
class DefaultLogoutEntryPointTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun `test node builder`() {
val entryPoint = DefaultLogoutEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
LogoutNode(
buildContext = buildContext,
plugins = plugins,
presenter = createLogoutPresenter(),
)
}
val callback = object : LogoutEntryPoint.Callback {
override fun navigateToSecureBackup() = lambdaError()
}
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
callback = callback,
)
assertThat(result).isInstanceOf(LogoutNode::class.java)
assertThat(result.plugins).contains(callback)
}
}
@@ -0,0 +1,121 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.logout.impl
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultLogoutUseCaseTest {
@Test
fun `test logout from one session`() = runTest {
val logoutLambda1 = lambdaRecorder<Boolean, Boolean, Unit> { _, _ -> }
val client1 = FakeMatrixClient(A_USER_ID).apply {
logoutLambda = logoutLambda1
}
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_USER_ID.value),
)
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
A_USER_ID -> Result.success(client1)
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
logoutLambda1.assertions().isCalledOnce().with(value(true), value(true))
}
@Test
fun `test logout from several sessions`() = runTest {
val logoutLambda1 = lambdaRecorder<Boolean, Boolean, Unit> { _, _ -> }
val logoutLambda2 = lambdaRecorder<Boolean, Boolean, Unit> { _, _ -> }
val client1 = FakeMatrixClient(A_USER_ID).apply {
logoutLambda = logoutLambda1
}
val client2 = FakeMatrixClient(A_USER_ID_2).apply {
logoutLambda = logoutLambda2
}
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_USER_ID.value),
aSessionData(sessionId = A_USER_ID_2.value),
)
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
A_USER_ID -> Result.success(client1)
A_USER_ID_2 -> Result.success(client2)
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
logoutLambda1.assertions().isCalledOnce().with(value(true), value(true))
logoutLambda2.assertions().isCalledOnce().with(value(true), value(true))
}
@Test
fun `test logout session not found is ignored`() = runTest {
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_USER_ID.value),
)
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
A_USER_ID -> Result.failure(Exception("Session not found"))
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
// No error
}
@Test
fun `test logout no sessions`() = runTest {
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = emptyList()
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
// No error
}
}
@@ -0,0 +1,246 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class LogoutPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createLogoutPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.isLastDevice).isFalse()
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
assertThat(initialState.doesBackupExistOnServer).isTrue()
assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.waitingForALongTime).isFalse()
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - initial state - last session`() = runTest {
val presenter = createLogoutPresenter(
encryptionService = FakeEncryptionService().apply {
emitIsLastDevice(true)
}
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.isLastDevice).isTrue()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - initial state - waiting a long time`() = runTest {
val encryptionService = FakeEncryptionService()
encryptionService.givenWaitForBackupUploadSteadyStateFlow(
flow {
emit(BackupUploadState.Waiting)
delay(3_000)
}
)
val presenter = createLogoutPresenter(
encryptionService = encryptionService
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.waitingForALongTime).isFalse()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
val waitingState = awaitItem()
assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
assertThat(initialState.waitingForALongTime).isFalse()
skipItems(1)
val waitingALongTimeState = awaitItem()
assertThat(waitingALongTimeState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
assertThat(waitingALongTimeState.waitingForALongTime).isTrue()
}
}
@Test
fun `present - initial state - backing up`() = runTest {
val encryptionService = FakeEncryptionService()
encryptionService.givenWaitForBackupUploadSteadyStateFlow(
flow {
emit(BackupUploadState.Waiting)
delay(1)
emit(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2))
delay(1)
emit(BackupUploadState.Done)
}
)
val presenter = createLogoutPresenter(
encryptionService = encryptionService
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isLastDevice).isFalse()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
val waitingState = awaitItem()
assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
skipItems(1)
val uploadingState = awaitItem()
assertThat(uploadingState.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2))
val doneState = awaitItem()
assertThat(doneState.backupUploadState).isEqualTo(BackupUploadState.Done)
}
}
@Test
fun `present - logout then cancel`() = runTest {
val presenter = createLogoutPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
initialState.eventSink.invoke(LogoutEvents.CloseDialogs)
val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - logout then confirm`() = runTest {
val cancelWorkManagerJobsLambda = lambdaRecorder<SessionId, Unit> {}
val workManagerScheduler = FakeWorkManagerScheduler(cancelLambda = cancelWorkManagerJobsLambda)
val presenter = createLogoutPresenter(workManagerScheduler = workManagerScheduler)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
cancelWorkManagerJobsLambda.assertions().isCalledOnce()
}
}
@Test
fun `present - logout with error then cancel`() = runTest {
val matrixClient = FakeMatrixClient().apply {
logoutLambda = { _, _ ->
throw AN_EXCEPTION
}
}
val presenter = createLogoutPresenter(
matrixClient,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val errorState = awaitItem()
assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
errorState.eventSink.invoke(LogoutEvents.CloseDialogs)
val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - logout with error then force`() = runTest {
val matrixClient = FakeMatrixClient().apply {
logoutLambda = { ignoreSdkError, _ ->
if (!ignoreSdkError) {
throw AN_EXCEPTION
}
}
}
val presenter = createLogoutPresenter(
matrixClient,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val errorState = awaitItem()
assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
errorState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = true))
val loadingState2 = awaitItem()
assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(2)
return awaitItem()
}
}
internal fun createLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(cancelLambda = {}),
): LogoutPresenter = LogoutPresenter(
matrixClient = matrixClient,
encryptionService = encryptionService,
workManagerScheduler = workManagerScheduler,
)
@@ -0,0 +1,126 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressTag
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LogoutViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on logout sends a LogoutEvents`() {
val eventsRecorder = EventsRecorder<LogoutEvents>()
rule.setLogoutView(
aLogoutState(
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_signout)
eventsRecorder.assertSingle(LogoutEvents.Logout(false))
}
@Test
fun `confirming logout sends a LogoutEvents`() {
val eventsRecorder = EventsRecorder<LogoutEvents>()
rule.setLogoutView(
aLogoutState(
logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder
),
)
rule.pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(LogoutEvents.Logout(false))
}
@Test
fun `clicking on back invoke back callback`() {
val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setLogoutView(
aLogoutState(
eventSink = eventsRecorder
),
onBackClick = callback,
)
rule.pressBack()
}
}
@Test
fun `clicking on confirm after error sends a LogoutEvents`() {
val eventsRecorder = EventsRecorder<LogoutEvents>()
rule.setLogoutView(
aLogoutState(
logoutAction = AsyncAction.Failure(Exception("Failed to logout")),
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_signout_anyway)
eventsRecorder.assertSingle(LogoutEvents.Logout(true))
}
@Test
fun `clicking on cancel after error sends a LogoutEvents`() {
val eventsRecorder = EventsRecorder<LogoutEvents>()
rule.setLogoutView(
aLogoutState(
logoutAction = AsyncAction.Failure(Exception("Failed to logout")),
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(LogoutEvents.CloseDialogs)
}
@Test
fun `last session setting button invoke onChangeRecoveryKeyClicked`() {
val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setLogoutView(
aLogoutState(
isLastDevice = true,
eventSink = eventsRecorder
),
onChangeRecoveryKeyClick = callback,
)
rule.clickOn(CommonStrings.common_settings)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLogoutView(
state: LogoutState,
onChangeRecoveryKeyClick: () -> Unit = EnsureNeverCalled(),
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
LogoutView(
state = state,
onChangeRecoveryKeyClick = onChangeRecoveryKeyClick,
onBackClick = onBackClick,
)
}
}
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl.direct
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBackKey
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DefaultDirectLogoutViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on confirm logout sends expected Event`() {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView(
state = aDirectLogoutState(
logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder,
)
)
rule.clickOn(CommonStrings.action_signout)
eventsRecorder.assertSingle(DirectLogoutEvents.Logout(false))
}
@Test
fun `clicking on cancel logout sends expected Event`() {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView(
state = aDirectLogoutState(
logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder,
)
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
}
@Ignore("Pressing back key should dismiss the dialog, and so generate the expected event, but it's not the case.")
@Test
fun `clicking on back invoke back callback`() {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView(
state = aDirectLogoutState(
logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder,
)
)
rule.pressBackKey()
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
}
@Test
fun `clicking on confirm after error sends expected Event`() {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView(
state = aDirectLogoutState(
logoutAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder,
)
)
rule.clickOn(CommonStrings.action_signout_anyway)
eventsRecorder.assertSingle(DirectLogoutEvents.Logout(true))
}
@Test
fun `clicking on cancel after error sends expected Event`() {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView(
state = aDirectLogoutState(
logoutAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder,
)
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDefaultDirectLogoutView(
state: DirectLogoutState,
) {
setContent {
DefaultDirectLogoutView().Render(state)
}
}
@@ -0,0 +1,188 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.logout.impl.direct
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class DirectLogoutPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createDirectLogoutPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.canDoDirectSignOut).isTrue()
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - initial state - last session`() = runTest {
val presenter = createDirectLogoutPresenter(
encryptionService = FakeEncryptionService().apply {
emitIsLastDevice(true)
}
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.canDoDirectSignOut).isFalse()
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - initial state - backing up`() = runTest {
val encryptionService = FakeEncryptionService()
encryptionService.givenWaitForBackupUploadSteadyStateFlow(
flow {
emit(BackupUploadState.Waiting)
}
)
val presenter = createDirectLogoutPresenter(
encryptionService = encryptionService
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitFirstItem()
assertThat(initialState.canDoDirectSignOut).isFalse()
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - logout then cancel`() = runTest {
val presenter = createDirectLogoutPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - logout then confirm`() = runTest {
val presenter = createDirectLogoutPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
}
}
@Test
fun `present - logout with error then cancel`() = runTest {
val matrixClient = FakeMatrixClient().apply {
logoutLambda = { _, _ ->
throw AN_EXCEPTION
}
}
val presenter = createDirectLogoutPresenter(
matrixClient,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val errorState = awaitItem()
assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
errorState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - logout with error then force`() = runTest {
val matrixClient = FakeMatrixClient().apply {
logoutLambda = { ignoreSdkError, _ ->
if (!ignoreSdkError) {
throw AN_EXCEPTION
}
}
}
val presenter = createDirectLogoutPresenter(
matrixClient,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val errorState = awaitItem()
assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
errorState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = true))
val loadingState2 = awaitItem()
assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
return awaitItem()
}
private fun createDirectLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(),
): DirectLogoutPresenter = DirectLogoutPresenter(
matrixClient = matrixClient,
encryptionService = encryptionService,
)
}