mirror of
https://github.com/ProtonMail/android-mail.git
synced 2026-05-15 09:50:40 +00:00
Merge remote-tracking branch 'upstream/main' into integrate-v6-changes
This commit is contained in:
@@ -74,6 +74,7 @@ scripts/uitests/AssetsFile.lock.bak
|
||||
.idea/androidTestResultsUserPreferences.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/deploymentTargetDropDown.xml
|
||||
.idea/deploymentTargetSelector.xml
|
||||
.idea/vcs.xml
|
||||
.idea/**/shelf
|
||||
.idea/runConfigurations.xml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"project": "android-mail-new",
|
||||
"locale": "e45c98b7fa1f33494847c289b8f821e418aa9608"
|
||||
"locale": "bee86db926775541bb26d476feaf29f7d7e3d418"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@ dependencies {
|
||||
androidTestImplementation(Dependencies.androidTestLibs)
|
||||
androidTestImplementation(Proton.Core.accountManagerPresentationCompose)
|
||||
androidTestImplementation(Proton.Core.accountRecoveryTest)
|
||||
androidTestImplementation(Proton.Core.authTest)
|
||||
androidTestImplementation(Proton.Core.planTest)
|
||||
androidTestImplementation(Proton.Core.reportTest)
|
||||
androidTestImplementation(Proton.Core.userRecoveryTest)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 36,
|
||||
"identityHash": "0a78184b5759a779486e235f8750efcd",
|
||||
"identityHash": "612b7eef8403ea1fad620c8f4b548cde",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "AccountEntity",
|
||||
@@ -323,6 +323,172 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "AuthDeviceEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `state` INTEGER NOT NULL, `name` TEXT NOT NULL, `localizedClientName` TEXT NOT NULL, `createdAtUtcSeconds` INTEGER NOT NULL, `activatedAtUtcSeconds` INTEGER, `rejectedAtUtcSeconds` INTEGER, `activationToken` TEXT, `lastActivityAtUtcSeconds` INTEGER NOT NULL, PRIMARY KEY(`userId`, `deviceId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceId",
|
||||
"columnName": "deviceId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "addressId",
|
||||
"columnName": "addressId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "state",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "localizedClientName",
|
||||
"columnName": "localizedClientName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAtUtcSeconds",
|
||||
"columnName": "createdAtUtcSeconds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activatedAtUtcSeconds",
|
||||
"columnName": "activatedAtUtcSeconds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "rejectedAtUtcSeconds",
|
||||
"columnName": "rejectedAtUtcSeconds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "activationToken",
|
||||
"columnName": "activationToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastActivityAtUtcSeconds",
|
||||
"columnName": "lastActivityAtUtcSeconds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"deviceId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_AuthDeviceEntity_userId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"userId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_AuthDeviceEntity_userId` ON `${TABLE_NAME}` (`userId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_AuthDeviceEntity_addressId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"addressId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_AuthDeviceEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "AccountEntity",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"userId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "DeviceSecretEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `secret` TEXT NOT NULL, `token` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "secret",
|
||||
"columnName": "secret",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DeviceSecretEntity_userId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"userId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_DeviceSecretEntity_userId` ON `${TABLE_NAME}` (`userId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "AccountEntity",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"userId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "UserEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `createdAtUtc` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `type` INTEGER, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, `maxBaseSpace` INTEGER, `maxDriveSpace` INTEGER, `usedBaseSpace` INTEGER, `usedDriveSpace` INTEGER, `recovery_state` INTEGER, `recovery_startTime` INTEGER, `recovery_endTime` INTEGER, `recovery_sessionId` TEXT, `recovery_reason` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
@@ -2765,7 +2931,7 @@
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0a78184b5759a779486e235f8750efcd')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '612b7eef8403ea1fad620c8f4b548cde')"
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -173,7 +173,7 @@ abstract class AppDatabase :
|
||||
companion object {
|
||||
|
||||
const val name = "db-mail"
|
||||
const val version = 36
|
||||
const val version = 37
|
||||
|
||||
internal val migrations = listOf(
|
||||
AppDatabaseMigrations.MIGRATION_1_2,
|
||||
@@ -210,7 +210,8 @@ abstract class AppDatabase :
|
||||
AppDatabaseMigrations.MIGRATION_32_33,
|
||||
AppDatabaseMigrations.MIGRATION_33_34,
|
||||
AppDatabaseMigrations.MIGRATION_34_35,
|
||||
AppDatabaseMigrations.MIGRATION_35_36
|
||||
AppDatabaseMigrations.MIGRATION_35_36,
|
||||
AppDatabaseMigrations.MIGRATION_36_37
|
||||
)
|
||||
|
||||
fun buildDatabase(context: Context): AppDatabase = databaseBuilder<AppDatabase>(context, name)
|
||||
|
||||
@@ -242,4 +242,9 @@ object AppDatabaseMigrations {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_36_37 = object : Migration(36, 37) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
@@ -54,7 +55,6 @@ import ch.protonmail.android.mailmessage.domain.model.MessageId
|
||||
import ch.protonmail.android.mailsidebar.presentation.Sidebar
|
||||
import ch.protonmail.android.navigation.model.Destination.Dialog
|
||||
import ch.protonmail.android.navigation.model.Destination.Screen
|
||||
import ch.protonmail.android.navigation.model.HomeState
|
||||
import ch.protonmail.android.navigation.route.addAlternativeRoutingSetting
|
||||
import ch.protonmail.android.navigation.route.addAppSettings
|
||||
import ch.protonmail.android.navigation.route.addAutoLockPinScreen
|
||||
@@ -96,7 +96,6 @@ import io.sentry.compose.withSentryObservableEffect
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.core.compose.component.ProtonSnackbarHostState
|
||||
import me.proton.core.compose.component.ProtonSnackbarType
|
||||
import me.proton.core.compose.flow.rememberAsState
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.network.domain.NetworkStatus
|
||||
|
||||
@@ -117,7 +116,7 @@ fun Home(
|
||||
val snackbarHostNormState = remember { ProtonSnackbarHostState(defaultType = ProtonSnackbarType.NORM) }
|
||||
val snackbarHostErrorState = remember { ProtonSnackbarHostState(defaultType = ProtonSnackbarType.ERROR) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = rememberAsState(flow = viewModel.state, initial = HomeState.Initial)
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val offlineSnackbarMessage = stringResource(id = R.string.you_are_offline)
|
||||
fun showOfflineSnackbar() = scope.launch {
|
||||
@@ -127,13 +126,13 @@ fun Home(
|
||||
)
|
||||
}
|
||||
|
||||
ConsumableLaunchedEffect(state.value.networkStatusEffect) {
|
||||
ConsumableLaunchedEffect(state.networkStatusEffect) {
|
||||
if (it == NetworkStatus.Disconnected) {
|
||||
showOfflineSnackbar()
|
||||
}
|
||||
}
|
||||
|
||||
ConsumableLaunchedEffect(state.value.navigateToEffect) {
|
||||
ConsumableLaunchedEffect(state.navigateToEffect) {
|
||||
viewModel.navigateTo(navController, it)
|
||||
}
|
||||
|
||||
@@ -245,7 +244,7 @@ fun Home(
|
||||
undoActionEffect.value = Effect.of(actionResult)
|
||||
}
|
||||
|
||||
ConsumableLaunchedEffect(state.value.messageSendingStatusEffect) { sendingStatus ->
|
||||
ConsumableLaunchedEffect(state.messageSendingStatusEffect) { sendingStatus ->
|
||||
when (sendingStatus) {
|
||||
is MessageSendingStatus.MessageSent -> showSuccessSendingMessageSnackbar()
|
||||
is MessageSendingStatus.SendMessageError -> showErrorSendingMessageSnackbar()
|
||||
|
||||
@@ -35,7 +35,7 @@ fun Launcher(activityActions: MainActivity.Actions, viewModel: LauncherViewModel
|
||||
|
||||
when (state) {
|
||||
LauncherState.AccountNeeded -> viewModel.submit(LauncherViewModel.Action.AddAccount)
|
||||
LauncherState.PrimaryExist -> Home(
|
||||
LauncherState.PrimaryExist -> LauncherRouter(
|
||||
activityActions = activityActions,
|
||||
launcherActions = Launcher.Actions(
|
||||
onPasswordManagement = { viewModel.submit(LauncherViewModel.Action.OpenPasswordManagement) },
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import ch.protonmail.android.MainActivity
|
||||
import ch.protonmail.android.navigation.model.OnboardingEligibilityState
|
||||
import ch.protonmail.android.navigation.onboarding.Onboarding
|
||||
import me.proton.core.compose.component.ProtonCenteredProgress
|
||||
|
||||
@Composable
|
||||
internal fun LauncherRouter(
|
||||
activityActions: MainActivity.Actions,
|
||||
launcherActions: Launcher.Actions,
|
||||
viewModel: LauncherRouterViewModel = hiltViewModel()
|
||||
) {
|
||||
|
||||
val onboardingState by viewModel.onboardingEligibilityState.collectAsStateWithLifecycle()
|
||||
|
||||
when (onboardingState) {
|
||||
OnboardingEligibilityState.Loading -> ProtonCenteredProgress(Modifier.fillMaxSize())
|
||||
OnboardingEligibilityState.NotRequired -> Home(activityActions, launcherActions)
|
||||
OnboardingEligibilityState.Required -> Onboarding()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.navigation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import arrow.core.Either
|
||||
import ch.protonmail.android.mailcommon.domain.model.PreferencesError
|
||||
import ch.protonmail.android.mailonboarding.domain.model.OnboardingPreference
|
||||
import ch.protonmail.android.mailonboarding.domain.usecase.ObserveOnboarding
|
||||
import ch.protonmail.android.navigation.model.OnboardingEligibilityState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class LauncherRouterViewModel @Inject constructor(
|
||||
observeOnboarding: ObserveOnboarding
|
||||
) : ViewModel() {
|
||||
|
||||
val onboardingEligibilityState = observeOnboarding()
|
||||
.mapLatest { it.toState() }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = OnboardingEligibilityState.Loading
|
||||
)
|
||||
|
||||
private fun Either<PreferencesError, OnboardingPreference>.toState(): OnboardingEligibilityState {
|
||||
val preference = this.getOrNull() ?: return OnboardingEligibilityState.Required
|
||||
return if (preference.display) OnboardingEligibilityState.Required else OnboardingEligibilityState.NotRequired
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,11 @@ sealed class Destination(val route: String) {
|
||||
object ManageMembers : Destination("contacts/group/manageMembers")
|
||||
|
||||
object ContactSearch : Destination("contacts/search")
|
||||
|
||||
object Onboarding {
|
||||
data object MainScreen : Destination("onboarding/main")
|
||||
data object Upselling : Destination("onboarding/upselling")
|
||||
}
|
||||
}
|
||||
|
||||
object Dialog {
|
||||
|
||||
+5
-9
@@ -16,14 +16,10 @@
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailonboarding.presentation.model
|
||||
package ch.protonmail.android.navigation.model
|
||||
|
||||
sealed interface OnboardingOperation {
|
||||
sealed interface Action : OnboardingOperation {
|
||||
data object CloseOnboarding : Action
|
||||
}
|
||||
|
||||
sealed interface Event : OnboardingOperation {
|
||||
data object ShowOnboarding : Event
|
||||
}
|
||||
internal sealed interface OnboardingEligibilityState {
|
||||
data object Required : OnboardingEligibilityState
|
||||
data object NotRequired : OnboardingEligibilityState
|
||||
data object Loading : OnboardingEligibilityState
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.navigation.onboarding
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import ch.protonmail.android.navigation.model.Destination
|
||||
import ch.protonmail.android.navigation.route.addOnboarding
|
||||
import ch.protonmail.android.navigation.route.addOnboardingUpselling
|
||||
import io.sentry.compose.withSentryObservableEffect
|
||||
|
||||
@Composable
|
||||
fun Onboarding() {
|
||||
val navController = rememberNavController().withSentryObservableEffect()
|
||||
val onboardingStepViewModel = hiltViewModel<OnboardingStepViewModel>()
|
||||
|
||||
val exitAction = remember {
|
||||
{
|
||||
onboardingStepViewModel.submit(OnboardingStepAction.MarkOnboardingComplete)
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
navController = navController,
|
||||
startDestination = Destination.Screen.Onboarding.MainScreen.route
|
||||
) {
|
||||
addOnboarding(navController, exitAction)
|
||||
addOnboardingUpselling(exitAction)
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.navigation.onboarding
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import ch.protonmail.android.mailonboarding.domain.usecase.SaveOnboarding
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class OnboardingStepViewModel @Inject constructor(
|
||||
private val saveOnboarding: SaveOnboarding
|
||||
) : ViewModel() {
|
||||
|
||||
fun submit(action: OnboardingStepAction) {
|
||||
viewModelScope.launch {
|
||||
when (action) {
|
||||
OnboardingStepAction.MarkOnboardingComplete -> saveOnboarding(display = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed interface OnboardingStepAction {
|
||||
data object MarkOnboardingComplete : OnboardingStepAction
|
||||
}
|
||||
@@ -592,4 +592,3 @@ internal fun NavGraphBuilder.addContactSearch(navController: NavHostController)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.navigation.route
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import ch.protonmail.android.mailonboarding.presentation.OnboardingScreen
|
||||
import ch.protonmail.android.mailupselling.presentation.ui.onboarding.OnboardingUpsellScreen
|
||||
import ch.protonmail.android.navigation.model.Destination
|
||||
|
||||
fun NavGraphBuilder.addOnboarding(navController: NavHostController, exitAction: () -> Unit) {
|
||||
composable(route = Destination.Screen.Onboarding.MainScreen.route) {
|
||||
OnboardingScreen(
|
||||
exitAction = exitAction,
|
||||
onUpsellingNavigation = {
|
||||
navController.navigate(Destination.Screen.Onboarding.Upselling.route) {
|
||||
popUpTo(Destination.Screen.Onboarding.MainScreen.route) { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.addOnboardingUpselling(exitAction: () -> Unit) {
|
||||
composable(route = Destination.Screen.Onboarding.Upselling.route) {
|
||||
OnboardingUpsellScreen(
|
||||
modifier = Modifier,
|
||||
exitScreen = exitAction
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Error al enviar el mensaje</string>
|
||||
<string name="mailbox_message_sending_error_action">Ir a Borradores</string>
|
||||
<string name="mailbox_attachment_uploading_error">Error al cargar el archivo adjunto</string>
|
||||
<string name="notification_switched_account">Cambiada a la cuenta: %s</string>
|
||||
<string name="notification_switched_account">Se cambió a la cuenta %s</string>
|
||||
<string name="label_saved">Etiqueta guardada</string>
|
||||
<string name="label_deleted">Etiqueta eliminada</string>
|
||||
<string name="label_list_loading_error">Error al cargar la etiqueta. Intente de nuevo más tarde.</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Памылка адпраўкі паведамлення</string>
|
||||
<string name="mailbox_message_sending_error_action">Перайсці ў Чарнавікі</string>
|
||||
<string name="mailbox_attachment_uploading_error">Памылка запампоўвання далучэння</string>
|
||||
<string name="notification_switched_account">Зменена на ўліковы запіс: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Метка захавана</string>
|
||||
<string name="label_deleted">Метка выдалена</string>
|
||||
<string name="label_list_loading_error">Памылка загрузкі меткі. Паспрабуйце пазней</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Error enviant el missatge</string>
|
||||
<string name="mailbox_message_sending_error_action">Aneu a Esborranys</string>
|
||||
<string name="mailbox_attachment_uploading_error">S\'ha produït un error en carregar l\'adjunt.</string>
|
||||
<string name="notification_switched_account">Canviat al compte: %s</string>
|
||||
<string name="notification_switched_account">S\'ha canviat al compte %s</string>
|
||||
<string name="label_saved">S\'ha desat l\'etiqueta.</string>
|
||||
<string name="label_deleted">S\'ha eliminat l\'etiqueta.</string>
|
||||
<string name="label_list_loading_error">S\'ha produït un error en carregar l\'etiqueta. Proveu de nou més tard.</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Chyba při odesílání zprávy</string>
|
||||
<string name="mailbox_message_sending_error_action">Přejít do Konceptů</string>
|
||||
<string name="mailbox_attachment_uploading_error">Chyba při nahrávání přílohy</string>
|
||||
<string name="notification_switched_account">Přepnuto na účet: \'%s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Štítek uložen</string>
|
||||
<string name="label_deleted">Štítek smazán</string>
|
||||
<string name="label_list_loading_error">Chyba při načítání štítku, zkuste to prosím později</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Besked om fejlafsendelse</string>
|
||||
<string name="mailbox_message_sending_error_action">Gå til kladder</string>
|
||||
<string name="mailbox_attachment_uploading_error">Fejl ved upload af vedhæftning</string>
|
||||
<string name="notification_switched_account">Skiftede til konto: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Etiket gemt</string>
|
||||
<string name="label_deleted">Etiket slettet</string>
|
||||
<string name="label_list_loading_error">Indlæsningsfejl af etikette. Prøv igen senere.</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Fehler beim Senden der Nachricht</string>
|
||||
<string name="mailbox_message_sending_error_action">Zu den Entwürfen</string>
|
||||
<string name="mailbox_attachment_uploading_error">Fehler beim Hochladen des Anhangs.</string>
|
||||
<string name="notification_switched_account">Zu Konto „%s“ gewechselt</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Kategorie gespeichert</string>
|
||||
<string name="label_deleted">Kategorie gelöscht</string>
|
||||
<string name="label_list_loading_error">Fehler beim Laden der Kategorie. Bitte versuche es später erneut.</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Σφάλμα κατά την αποστολή του μηνύματος</string>
|
||||
<string name="mailbox_message_sending_error_action">Μετάβαση στα Πρόχειρα</string>
|
||||
<string name="mailbox_attachment_uploading_error">Σφάλμα κατά τη μεταφόρτωση συνημμένου</string>
|
||||
<string name="notification_switched_account">Έγινε αλλαγή σε λογαριασμό: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Η ετικέτα αποθηκεύτηκε</string>
|
||||
<string name="label_deleted">Η ετικέτα διαγράφηκε</string>
|
||||
<string name="label_list_loading_error">Σφάλμα φόρτωσης ετικέτας, παρακαλούμε δοκιμάστε ξανά αργότερα</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Error al enviar el mensaje</string>
|
||||
<string name="mailbox_message_sending_error_action">Ir a la carpeta de borradores</string>
|
||||
<string name="mailbox_attachment_uploading_error">Error al cargar el archivo adjunto</string>
|
||||
<string name="notification_switched_account">Se ha cambiado a la cuenta: %s</string>
|
||||
<string name="notification_switched_account">Se ha cambiado a la cuenta %s</string>
|
||||
<string name="label_saved">Se ha guardado la etiqueta.</string>
|
||||
<string name="label_deleted">Se ha eliminado la etiqueta.</string>
|
||||
<string name="label_list_loading_error">Error al cargar la etiqueta. Intenta de nuevo más tarde.</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Pogreška pri slanju poruke</string>
|
||||
<string name="mailbox_message_sending_error_action">Idi u skice</string>
|
||||
<string name="mailbox_attachment_uploading_error">Error uploading attachment</string>
|
||||
<string name="notification_switched_account">Prebačeno na račun %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Oznaka spremljena</string>
|
||||
<string name="label_deleted">Oznaka izbrisana</string>
|
||||
<string name="label_list_loading_error">Pogreška pri učitavanju oznake, pokušajte ponovno kasnije</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Hiba az üzenet küldése közben</string>
|
||||
<string name="mailbox_message_sending_error_action">Ugrás a piszkozatokhoz</string>
|
||||
<string name="mailbox_attachment_uploading_error">Hiba mellékletek feltöltésénél</string>
|
||||
<string name="notification_switched_account">Átlépve a fiókba: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Címke mentve</string>
|
||||
<string name="label_deleted">Címke törölve</string>
|
||||
<string name="label_list_loading_error">Hiba a címke betöltése során, próbálja újra később</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Masalah dalam mengirim pesan</string>
|
||||
<string name="mailbox_message_sending_error_action">Ke Draf</string>
|
||||
<string name="mailbox_attachment_uploading_error">Kesalahan mengunggah lampiran</string>
|
||||
<string name="notification_switched_account">Anda telah beralih ke akun: \'%s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Label disimpan</string>
|
||||
<string name="label_deleted">Label dihapus</string>
|
||||
<string name="label_list_loading_error">Terdapat kesalahan dalam memuat label, silakan coba lagi nanti</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Invio del messaggio non riuscito</string>
|
||||
<string name="mailbox_message_sending_error_action">Vai alle bozze</string>
|
||||
<string name="mailbox_attachment_uploading_error">Caricamento dell\'allegato non riuscito</string>
|
||||
<string name="notification_switched_account">Account attuale: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Etichetta salvata</string>
|
||||
<string name="label_deleted">Etichetta eliminata</string>
|
||||
<string name="label_list_loading_error">Caricamento dell\'etichetta non riuscito</string>
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<string name="mailbox_message_sending_error">メッセージ送信エラー</string>
|
||||
<string name="mailbox_message_sending_error_action">下書きを開く</string>
|
||||
<string name="mailbox_attachment_uploading_error">添付ファイルのアップロードエラー</string>
|
||||
<string name="notification_switched_account">アカウントを %s に切り替えました</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">ラベルが保存されました</string>
|
||||
<string name="label_deleted">ラベルが削除されました</string>
|
||||
<string name="label_list_loading_error">Error loading label, please try again later</string>
|
||||
<string name="label_list_loading_error">ラベルの読み込み中にエラーが発生しました。後でもう一度お試しください。</string>
|
||||
<string name="app_locked">アプリはロックされています。</string>
|
||||
<string name="use_pin_instead">代わりにPINコードを使用する</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">შეტყობინების გაგზავნის შეცდომა</string>
|
||||
<string name="mailbox_message_sending_error_action">მონახაზებზე გადასვლა</string>
|
||||
<string name="mailbox_attachment_uploading_error">მიმაგრებული ფაილის ატვირთვის შეცდომა</string>
|
||||
<string name="notification_switched_account">გადაერთეთ ანგარიშზე: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">ჭდე შენახულია</string>
|
||||
<string name="label_deleted">ჭდე წაშლილია</string>
|
||||
<string name="label_list_loading_error">ჭდის ჩატვირთვის შეცდომა. მოგვიანებით სცადეთ</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Yalla-d tuccḍa deg tuzna n yizen</string>
|
||||
<string name="mailbox_message_sending_error_action">Ddu ɣer Yirewwayen</string>
|
||||
<string name="mailbox_attachment_uploading_error">Error uploading attachment</string>
|
||||
<string name="notification_switched_account">Tnekzeḍ ɣer umiḍan: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Label saved</string>
|
||||
<string name="label_deleted">Tabzimt tettwakkes</string>
|
||||
<string name="label_list_loading_error">Error loading label, please try again later</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">메시지 전송 오류</string>
|
||||
<string name="mailbox_message_sending_error_action">임시 보관함으로 이동하기</string>
|
||||
<string name="mailbox_attachment_uploading_error">첨부 파일 업로드 중 오류</string>
|
||||
<string name="notification_switched_account">%s 계정으로 전환됨</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">라벨을 저장했습니다.</string>
|
||||
<string name="label_deleted">라벨을 삭제했습니다.</string>
|
||||
<string name="label_list_loading_error">라벨 로드 중 오류 발생, 다시 시도하십시오.</string>
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<string name="mailbox_message_sending_error">Feil ved sending av melding</string>
|
||||
<string name="mailbox_message_sending_error_action">Gå til utkast</string>
|
||||
<string name="mailbox_attachment_uploading_error">Feil ved opplasting av vedlegg</string>
|
||||
<string name="notification_switched_account">Byttet til konto: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Etikett lagret</string>
|
||||
<string name="label_deleted">Etikett slettet</string>
|
||||
<string name="label_list_loading_error">Feil ved lasting av etikett, vennligst prøv igjen senere.</string>
|
||||
<string name="label_list_loading_error">Feil ved lasting av etikett. Prøv igjen senere.</string>
|
||||
<string name="app_locked">App låst</string>
|
||||
<string name="use_pin_instead">Bruk PIN-kode istedenfor</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Fout bij verzenden bericht</string>
|
||||
<string name="mailbox_message_sending_error_action">Ga naar Concepten</string>
|
||||
<string name="mailbox_attachment_uploading_error">Fout bij het uploaden van bijlage</string>
|
||||
<string name="notification_switched_account">Overgeschakeld naar account: %s</string>
|
||||
<string name="notification_switched_account">Overgeschakeld naar account %s</string>
|
||||
<string name="label_saved">Label opgeslagen</string>
|
||||
<string name="label_deleted">Label verwijderd</string>
|
||||
<string name="label_list_loading_error">Fout tijdens laden label, probeer het later opnieuw</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Wystąpił błąd podczas wysyłania wiadomości</string>
|
||||
<string name="mailbox_message_sending_error_action">Przejdź do Szkiców</string>
|
||||
<string name="mailbox_attachment_uploading_error">Wystąpił błąd podczas przesyłania załącznika</string>
|
||||
<string name="notification_switched_account">Przełączono na konto %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Etykieta została zapisana</string>
|
||||
<string name="label_deleted">Etykieta została usunięta</string>
|
||||
<string name="label_list_loading_error">Wystąpił błąd podczas ładowania etykiety. Spróbuj ponownie później</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Erro no envio da mensagem</string>
|
||||
<string name="mailbox_message_sending_error_action">Ir para Rascunhos</string>
|
||||
<string name="mailbox_attachment_uploading_error">Erro ao enviar anexo</string>
|
||||
<string name="notification_switched_account">Mudou para a conta %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Marcador salvo</string>
|
||||
<string name="label_deleted">Marcador excluído</string>
|
||||
<string name="label_list_loading_error">Erro ao carregar o marcador, tente novamente mais tarde</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Erro no envio da mensagem</string>
|
||||
<string name="mailbox_message_sending_error_action">Ir para Rascunhos</string>
|
||||
<string name="mailbox_attachment_uploading_error">Erro a enviar o anexo</string>
|
||||
<string name="notification_switched_account">Mudou para a conta: \'%s\'.</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Etiqueta guardada</string>
|
||||
<string name="label_deleted">Etiqueta eliminada</string>
|
||||
<string name="label_list_loading_error">Erro ao carregar a etiqueta, por favor, tente outra vez mais tarde</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Eroare trimitere mesaj</string>
|
||||
<string name="mailbox_message_sending_error_action">Accesare Ciorne</string>
|
||||
<string name="mailbox_attachment_uploading_error">Eroare la încărcare atașament.</string>
|
||||
<string name="notification_switched_account">S-a comutat la contul: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Etichetă a fost salvată.</string>
|
||||
<string name="label_deleted">Etichetă a fost ștearsă.</string>
|
||||
<string name="label_list_loading_error">Eroare la încărcarea etichetei. Reîncercați mai târziu.</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Ошибка при отправке сообщения</string>
|
||||
<string name="mailbox_message_sending_error_action">Перейти в Черновики</string>
|
||||
<string name="mailbox_attachment_uploading_error">Ошибка загрузки вложения</string>
|
||||
<string name="notification_switched_account">Переключено на аккаунт: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Ярлык сохранён</string>
|
||||
<string name="label_deleted">Ярлык удалён</string>
|
||||
<string name="label_list_loading_error">Ошибка загрузки ярлыка, повторите попытку позже</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Chyba pri odosielaní správy</string>
|
||||
<string name="mailbox_message_sending_error_action">Prejsť do zložky Koncepty</string>
|
||||
<string name="mailbox_attachment_uploading_error">Chyba pri nahrávaní prílohy</string>
|
||||
<string name="notification_switched_account">Prepnuté na účet: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Štítok uložený</string>
|
||||
<string name="label_deleted">Štítok odstránený</string>
|
||||
<string name="label_list_loading_error">Chyba pri načítaní štítku, skúste to znova neskôr, prosím</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Napaka pri pošiljanju sporočila</string>
|
||||
<string name="mailbox_message_sending_error_action">Pojdi v mapo Osnutki</string>
|
||||
<string name="mailbox_attachment_uploading_error">Napaka pri nalaganju priponke</string>
|
||||
<string name="notification_switched_account">Preklopili ste na račun: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Oznaka shranjena</string>
|
||||
<string name="label_deleted">Oznaka izbrisana</string>
|
||||
<string name="label_list_loading_error">Napaka pri nalaganju oznake, poskusite znova pozneje</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Det gick inte att skicka meddelandet</string>
|
||||
<string name="mailbox_message_sending_error_action">Gå till utkast</string>
|
||||
<string name="mailbox_attachment_uploading_error">Kunde inte ladda upp bilaga</string>
|
||||
<string name="notification_switched_account">Bytte till konto: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Etikett sparad</string>
|
||||
<string name="label_deleted">Etikett borttagen</string>
|
||||
<string name="label_list_loading_error">Gick inte ladda etikett, försök igen senare</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">İleti gönderilirken sorun çıktı</string>
|
||||
<string name="mailbox_message_sending_error_action">Taslaklar kutusuna git</string>
|
||||
<string name="mailbox_attachment_uploading_error">Ek dosya yüklenirken sorun çıktı</string>
|
||||
<string name="notification_switched_account">Geçilen hesap: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Etiket kaydedldi</string>
|
||||
<string name="label_deleted">Etiket silindi</string>
|
||||
<string name="label_list_loading_error">Etiket yüklenirken sorun çıktı. Lütfen bir süre sonra yeniden deneyin</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">Помилка надсилання повідомлення</string>
|
||||
<string name="mailbox_message_sending_error_action">Перейти до Чернеток</string>
|
||||
<string name="mailbox_attachment_uploading_error">Помилка вивантаження вкладення</string>
|
||||
<string name="notification_switched_account">Змінено на обліковий запис: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Мітку збережено</string>
|
||||
<string name="label_deleted">Мітку видалено</string>
|
||||
<string name="label_list_loading_error">Помилка завантаження мітки, спробуйте знову пізніше</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">发送信息时出错</string>
|
||||
<string name="mailbox_message_sending_error_action">转至草稿箱</string>
|
||||
<string name="mailbox_attachment_uploading_error">上传附件时出错</string>
|
||||
<string name="notification_switched_account">已转至 %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">标签已保存</string>
|
||||
<string name="label_deleted">标签已删除</string>
|
||||
<string name="label_list_loading_error">加载标签出错。请稍后再试。</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="mailbox_message_sending_error">傳送郵件時發生錯誤</string>
|
||||
<string name="mailbox_message_sending_error_action">前往草稿資料夾</string>
|
||||
<string name="mailbox_attachment_uploading_error">上載附件出現錯誤</string>
|
||||
<string name="notification_switched_account">已切換至帳號「%s」</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">已儲存標籤</string>
|
||||
<string name="label_deleted">已刪除標籤</string>
|
||||
<string name="label_list_loading_error">載入標籤時發生錯誤,請稍後再試。</string>
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
<bool name="core_feature_notifications_enabled">true</bool>
|
||||
<!-- Mail already handle the Notification Permission (after successful login). -->
|
||||
<bool name="core_feature_notifications_permission_request_enabled">false</bool>
|
||||
<integer name="core_feature_auth_user_check_max_free_user_count">2</integer>
|
||||
</resources>
|
||||
@@ -37,11 +37,10 @@
|
||||
<string name="mailbox_message_sending_error">Error sending message</string>
|
||||
<string name="mailbox_message_sending_error_action">Go to Drafts</string>
|
||||
<string name="mailbox_attachment_uploading_error">Error uploading attachment</string>
|
||||
<string name="notification_switched_account">Switched to account: %s</string>
|
||||
<string name="notification_switched_account">Switched to account %s</string>
|
||||
<string name="label_saved">Label saved</string>
|
||||
<string name="label_deleted">Label deleted</string>
|
||||
<string name="label_list_loading_error">Error loading label, please try again later</string>
|
||||
<string name="app_locked">App locked</string>
|
||||
<string name="use_pin_instead">Use PIN instead</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.navigation
|
||||
|
||||
import app.cash.turbine.test
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import ch.protonmail.android.mailcommon.domain.model.PreferencesError
|
||||
import ch.protonmail.android.mailonboarding.domain.model.OnboardingPreference
|
||||
import ch.protonmail.android.mailonboarding.domain.usecase.ObserveOnboarding
|
||||
import ch.protonmail.android.navigation.model.OnboardingEligibilityState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal class LauncherRouterViewModelTest {
|
||||
|
||||
private val observeOnboarding = mockk<ObserveOnboarding>()
|
||||
|
||||
private val viewModel: LauncherRouterViewModel
|
||||
get() = LauncherRouterViewModel(observeOnboarding)
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(UnconfinedTestDispatcher())
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun teardown() {
|
||||
Dispatchers.resetMain()
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit loading on observation start`() = runTest {
|
||||
// Given
|
||||
every { observeOnboarding() } returns flowOf()
|
||||
|
||||
// When + Then
|
||||
viewModel.onboardingEligibilityState.test {
|
||||
assertEquals(OnboardingEligibilityState.Loading, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit an onboarding required state when observe onboarding returns true`() = runTest {
|
||||
// Given
|
||||
every { observeOnboarding() } returns flowOf(OnboardingPreference(true).right())
|
||||
|
||||
// When + Then
|
||||
viewModel.onboardingEligibilityState.test {
|
||||
assertEquals(OnboardingEligibilityState.Required, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit a non onboarding required state when observe onboarding returns false`() = runTest {
|
||||
// Given
|
||||
every { observeOnboarding() } returns flowOf(OnboardingPreference(false).right())
|
||||
|
||||
// When + Then
|
||||
viewModel.onboardingEligibilityState.test {
|
||||
assertEquals(OnboardingEligibilityState.NotRequired, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit an onboarding required state when observe onboarding errors`() = runTest {
|
||||
// Given
|
||||
every { observeOnboarding() } returns flowOf(PreferencesError.left())
|
||||
|
||||
// When + Then
|
||||
viewModel.onboardingEligibilityState.test {
|
||||
assertEquals(OnboardingEligibilityState.Required, awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.navigation
|
||||
|
||||
import ch.protonmail.android.mailonboarding.domain.usecase.SaveOnboarding
|
||||
import ch.protonmail.android.navigation.onboarding.OnboardingStepAction
|
||||
import ch.protonmail.android.navigation.onboarding.OnboardingStepViewModel
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.confirmVerified
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
internal class OnboardingStepViewModelTest {
|
||||
|
||||
private val saveOnboarding = mockk<SaveOnboarding>(relaxed = true)
|
||||
private val viewModel = OnboardingStepViewModel(saveOnboarding)
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
Dispatchers.setMain(UnconfinedTestDispatcher())
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun teardown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should call save onboarding when marking onboarding as completed`() = runTest {
|
||||
// When
|
||||
viewModel.submit(OnboardingStepAction.MarkOnboardingComplete)
|
||||
|
||||
// Then
|
||||
coVerify(exactly = 1) { saveOnboarding(false) }
|
||||
confirmVerified(saveOnboarding)
|
||||
}
|
||||
}
|
||||
+2
-55
@@ -29,10 +29,7 @@ import ch.protonmail.android.uitest.helpers.core.navigation.Destination
|
||||
import ch.protonmail.android.uitest.helpers.core.navigation.navigator
|
||||
import ch.protonmail.android.uitest.helpers.network.mockNetworkDispatcher
|
||||
import ch.protonmail.android.uitest.robot.composer.composerRobot
|
||||
import ch.protonmail.android.uitest.robot.composer.section.attachmentsBottomSheet
|
||||
import ch.protonmail.android.uitest.robot.composer.section.recipients.toRecipientSection
|
||||
import ch.protonmail.android.uitest.robot.composer.section.topAppBarSection
|
||||
import ch.protonmail.android.uitest.robot.composer.section.verify
|
||||
import ch.protonmail.android.uitest.robot.detail.model.attachments.AttachmentDetailItemEntry
|
||||
import ch.protonmail.android.uitest.robot.detail.section.attachmentsSection
|
||||
import ch.protonmail.android.uitest.robot.detail.section.verify
|
||||
@@ -45,14 +42,13 @@ import dagger.hilt.android.testing.UninstallModules
|
||||
import io.mockk.mockk
|
||||
import me.proton.core.auth.domain.usecase.ValidateServerProof
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@RegressionTest
|
||||
@SdkSuppress(minSdkVersion = 30, maxSdkVersion = 32)
|
||||
@HiltAndroidTest
|
||||
@UninstallModules(ServerProofModule::class)
|
||||
internal class ComposerAttachmentsBottomSheetTests : MockedNetworkTest(), ComposerAttachmentsTests {
|
||||
internal class ComposerAttachmentsButtonTests : MockedNetworkTest(), ComposerAttachmentsTests {
|
||||
|
||||
@JvmField
|
||||
@BindValue
|
||||
@@ -79,14 +75,9 @@ internal class ComposerAttachmentsBottomSheetTests : MockedNetworkTest(), Compos
|
||||
|
||||
@Test
|
||||
@TestId("226087", "226088")
|
||||
fun testMainAttachmentsBottomSheetInteractions() {
|
||||
fun testMainAttachmentsButtonInteractions() {
|
||||
composerRobot {
|
||||
topAppBarSection { tapAttachmentsButton() }
|
||||
|
||||
attachmentsBottomSheet {
|
||||
verify { hasImportOption() }
|
||||
tapImportItem()
|
||||
}
|
||||
}
|
||||
|
||||
deviceRobot {
|
||||
@@ -94,54 +85,12 @@ internal class ComposerAttachmentsBottomSheetTests : MockedNetworkTest(), Compos
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("To be enabled again when MAILANDR-1101 is addressed.")
|
||||
@TestId("226089")
|
||||
fun testAttachmentsBottomSheetDismissalWithBackButton() {
|
||||
composerRobot {
|
||||
topAppBarSection { tapAttachmentsButton() }
|
||||
|
||||
attachmentsBottomSheet {
|
||||
verify { isShown() }
|
||||
}
|
||||
}
|
||||
|
||||
deviceRobot { pressBack() }
|
||||
|
||||
composerRobot {
|
||||
attachmentsBottomSheet {
|
||||
verify { isDismissed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestId("226090")
|
||||
fun testAttachmentsBottomSheetDismissalWithExternalTap() {
|
||||
composerRobot {
|
||||
topAppBarSection { tapAttachmentsButton() }
|
||||
|
||||
attachmentsBottomSheet {
|
||||
verify { isShown() }
|
||||
}
|
||||
|
||||
toRecipientSection { expandCcAndBccFields() }
|
||||
}
|
||||
|
||||
composerRobot {
|
||||
attachmentsBottomSheet {
|
||||
verify { isDismissed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SdkSuppress(minSdkVersion = 29)
|
||||
@TestId("226090")
|
||||
fun testAttachmentChipEntryUponPicking() {
|
||||
composerRobot {
|
||||
topAppBarSection { tapAttachmentsButton() }
|
||||
attachmentsBottomSheet { tapImportItem() }
|
||||
attachmentsSection { verify { hasAttachments(defaultExpectedEntry) } }
|
||||
}
|
||||
}
|
||||
@@ -157,11 +106,9 @@ internal class ComposerAttachmentsBottomSheetTests : MockedNetworkTest(), Compos
|
||||
|
||||
composerRobot {
|
||||
topAppBarSection { tapAttachmentsButton() }
|
||||
attachmentsBottomSheet { tapImportItem() }
|
||||
attachmentsSection { verify { hasAttachments(defaultExpectedEntry) } }
|
||||
|
||||
topAppBarSection { tapAttachmentsButton() }
|
||||
attachmentsBottomSheet { tapImportItem() }
|
||||
attachmentsSection { verify { hasAttachments(*expectedEntries) } }
|
||||
}
|
||||
}
|
||||
-2
@@ -38,7 +38,6 @@ import ch.protonmail.android.uitest.robot.common.section.verify
|
||||
import ch.protonmail.android.uitest.robot.composer.ComposerRobot
|
||||
import ch.protonmail.android.uitest.robot.composer.composerRobot
|
||||
import ch.protonmail.android.uitest.robot.composer.model.snackbar.ComposerSnackbar
|
||||
import ch.protonmail.android.uitest.robot.composer.section.attachmentsBottomSheet
|
||||
import ch.protonmail.android.uitest.robot.composer.section.messageBodySection
|
||||
import ch.protonmail.android.uitest.robot.composer.section.recipients.toRecipientSection
|
||||
import ch.protonmail.android.uitest.robot.composer.section.subjectSection
|
||||
@@ -167,6 +166,5 @@ internal class ComposerSendMessageWithAttachmentsTests :
|
||||
messageBodySection { typeMessageBody(body) }
|
||||
|
||||
topAppBarSection { tapAttachmentsButton() }
|
||||
attachmentsBottomSheet { tapImportItem() }
|
||||
}
|
||||
}
|
||||
|
||||
-7
@@ -25,12 +25,9 @@ import ch.protonmail.android.uitest.MockedNetworkTest
|
||||
import ch.protonmail.android.uitest.helpers.core.TestId
|
||||
import ch.protonmail.android.uitest.helpers.core.navigation.Destination
|
||||
import ch.protonmail.android.uitest.helpers.core.navigation.navigator
|
||||
import ch.protonmail.android.uitest.robot.common.section.snackbarSection
|
||||
import ch.protonmail.android.uitest.robot.common.section.verify
|
||||
import ch.protonmail.android.uitest.robot.composer.composerRobot
|
||||
import ch.protonmail.android.uitest.robot.composer.model.chips.RecipientChipEntry
|
||||
import ch.protonmail.android.uitest.robot.composer.model.chips.RecipientChipValidationState
|
||||
import ch.protonmail.android.uitest.robot.composer.model.snackbar.ComposerSnackbar
|
||||
import ch.protonmail.android.uitest.robot.composer.section.recipients.bccRecipientSection
|
||||
import ch.protonmail.android.uitest.robot.composer.section.recipients.ccRecipientSection
|
||||
import ch.protonmail.android.uitest.robot.composer.section.recipients.toRecipientSection
|
||||
@@ -113,10 +110,6 @@ internal class ComposerRecipientsDuplicatedChipsTests : MockedNetworkTest(), Com
|
||||
recipientChipIsNotDisplayed(expectedNotExistsChip)
|
||||
}
|
||||
}
|
||||
|
||||
snackbarSection {
|
||||
verify { isDisplaying(ComposerSnackbar.DuplicateEmailAddress(expectedFocusedRecipientChip.text)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-14
@@ -26,13 +26,10 @@ import ch.protonmail.android.uitest.MockedNetworkTest
|
||||
import ch.protonmail.android.uitest.helpers.core.TestId
|
||||
import ch.protonmail.android.uitest.helpers.core.navigation.Destination
|
||||
import ch.protonmail.android.uitest.helpers.core.navigation.navigator
|
||||
import ch.protonmail.android.uitest.robot.common.section.snackbarSection
|
||||
import ch.protonmail.android.uitest.robot.common.section.verify
|
||||
import ch.protonmail.android.uitest.robot.composer.composerRobot
|
||||
import ch.protonmail.android.uitest.robot.composer.model.chips.ChipsCreationTrigger
|
||||
import ch.protonmail.android.uitest.robot.composer.model.chips.RecipientChipEntry
|
||||
import ch.protonmail.android.uitest.robot.composer.model.chips.RecipientChipValidationState
|
||||
import ch.protonmail.android.uitest.robot.composer.model.snackbar.ComposerSnackbar
|
||||
import ch.protonmail.android.uitest.robot.composer.section.recipients.ComposerRecipientsSection
|
||||
import ch.protonmail.android.uitest.robot.composer.section.recipients.bccRecipientSection
|
||||
import ch.protonmail.android.uitest.robot.composer.section.recipients.ccRecipientSection
|
||||
@@ -75,8 +72,6 @@ internal class ComposerRecipientsInvalidChipsTests : MockedNetworkTest(), Compos
|
||||
toRecipientSection {
|
||||
createAndVerifyInvalidChip()
|
||||
}
|
||||
|
||||
snackbarSection { verify { isDisplaying(ComposerSnackbar.InvalidEmailAddress) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +86,6 @@ internal class ComposerRecipientsInvalidChipsTests : MockedNetworkTest(), Compos
|
||||
ccRecipientSection {
|
||||
createAndVerifyInvalidChip()
|
||||
}
|
||||
|
||||
snackbarSection { verify { isDisplaying(ComposerSnackbar.InvalidEmailAddress) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,8 +100,6 @@ internal class ComposerRecipientsInvalidChipsTests : MockedNetworkTest(), Compos
|
||||
bccRecipientSection {
|
||||
createAndVerifyInvalidChip()
|
||||
}
|
||||
|
||||
snackbarSection { verify { isDisplaying(ComposerSnackbar.InvalidEmailAddress) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,8 +119,6 @@ internal class ComposerRecipientsInvalidChipsTests : MockedNetworkTest(), Compos
|
||||
toRecipientSection {
|
||||
verify { hasRecipientChips(expectedRecipientChip) }
|
||||
}
|
||||
|
||||
snackbarSection { verify { isDisplaying(ComposerSnackbar.InvalidEmailAddress) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,8 +129,6 @@ internal class ComposerRecipientsInvalidChipsTests : MockedNetworkTest(), Compos
|
||||
toRecipientSection {
|
||||
createAndVerifyInvalidChip(trigger = ChipsCreationTrigger.Spacebar)
|
||||
}
|
||||
|
||||
snackbarSection { verify { isDisplaying(ComposerSnackbar.InvalidEmailAddress) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +141,6 @@ internal class ComposerRecipientsInvalidChipsTests : MockedNetworkTest(), Compos
|
||||
typeRecipient(it.text, autoConfirm = true)
|
||||
|
||||
verify { hasRecipientChips(it) }
|
||||
snackbarSection { verify { isDisplaying(ComposerSnackbar.InvalidEmailAddress) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.uitest.e2e.mailbox.detail.attachments
|
||||
|
||||
import ch.protonmail.android.di.ServerProofModule
|
||||
import ch.protonmail.android.networkmocks.mockwebserver.combineWith
|
||||
import ch.protonmail.android.networkmocks.mockwebserver.requests.MimeType
|
||||
import ch.protonmail.android.networkmocks.mockwebserver.requests.get
|
||||
import ch.protonmail.android.networkmocks.mockwebserver.requests.ignoreQueryParams
|
||||
import ch.protonmail.android.networkmocks.mockwebserver.requests.matchWildcards
|
||||
import ch.protonmail.android.networkmocks.mockwebserver.requests.respondWith
|
||||
import ch.protonmail.android.networkmocks.mockwebserver.requests.serveOnce
|
||||
import ch.protonmail.android.networkmocks.mockwebserver.requests.withMimeType
|
||||
import ch.protonmail.android.networkmocks.mockwebserver.requests.withStatusCode
|
||||
import ch.protonmail.android.test.annotations.suite.RegressionTest
|
||||
import ch.protonmail.android.test.annotations.suite.SmokeTest
|
||||
import ch.protonmail.android.uitest.MockedNetworkTest
|
||||
import ch.protonmail.android.uitest.helpers.core.TestId
|
||||
import ch.protonmail.android.uitest.helpers.core.navigation.Destination
|
||||
import ch.protonmail.android.uitest.helpers.core.navigation.navigator
|
||||
import ch.protonmail.android.uitest.helpers.login.LoginTestUserTypes
|
||||
import ch.protonmail.android.uitest.helpers.network.mockNetworkDispatcher
|
||||
import ch.protonmail.android.uitest.robot.detail.messageDetailRobot
|
||||
import ch.protonmail.android.uitest.robot.detail.section.attachmentsSection
|
||||
import ch.protonmail.android.uitest.robot.detail.section.messageBodySection
|
||||
import ch.protonmail.android.uitest.robot.helpers.deviceRobot
|
||||
import ch.protonmail.android.uitest.robot.helpers.section.intents
|
||||
import ch.protonmail.android.uitest.robot.helpers.section.verify
|
||||
import dagger.hilt.android.testing.BindValue
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import dagger.hilt.android.testing.UninstallModules
|
||||
import io.mockk.mockk
|
||||
import me.proton.core.auth.domain.usecase.ValidateServerProof
|
||||
import kotlin.test.Test
|
||||
|
||||
@RegressionTest
|
||||
@UninstallModules(ServerProofModule::class)
|
||||
@HiltAndroidTest
|
||||
internal class AttachmentMessageModeTests : MockedNetworkTest(loginType = LoginTestUserTypes.Paid.FancyCapybara) {
|
||||
|
||||
@JvmField
|
||||
@BindValue
|
||||
val serverProofValidation: ValidateServerProof = mockk(relaxUnitFun = true)
|
||||
|
||||
@Test
|
||||
@SmokeTest
|
||||
@TestId("417285")
|
||||
fun testAttachmentOpeningWithHeaderMaps() {
|
||||
mockWebServer.dispatcher combineWith mockNetworkDispatcher(useDefaultMailSettings = false) {
|
||||
addMockRequests(
|
||||
get("/mail/v4/settings")
|
||||
respondWith "/mail/v4/settings/mail-v4-settings_placeholder_messages.json"
|
||||
withStatusCode 200,
|
||||
get("/mail/v4/messages")
|
||||
respondWith "/mail/v4/messages/messages_417285.json"
|
||||
withStatusCode 200 ignoreQueryParams true,
|
||||
get("/mail/v4/messages/*")
|
||||
respondWith "/mail/v4/messages/message-id/message-id_417285.json"
|
||||
withStatusCode 200 matchWildcards true serveOnce true,
|
||||
get("/mail/v4/attachments/*")
|
||||
respondWith "/mail/v4/attachments/attachment_417285"
|
||||
withStatusCode 200 matchWildcards true serveOnce true
|
||||
withMimeType MimeType.OctetStream
|
||||
)
|
||||
}
|
||||
|
||||
navigator { navigateTo(Destination.MailDetail()) }
|
||||
|
||||
messageDetailRobot {
|
||||
messageBodySection { waitUntilMessageIsShown() }
|
||||
attachmentsSection { tapItem() }
|
||||
deviceRobot {
|
||||
intents { verify { actionViewIntentWasLaunched(times = 1) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -39,10 +39,18 @@ internal interface OpenExistingDraftsTest {
|
||||
) {
|
||||
toRecipientSection {
|
||||
verify { hasRecipientChips(toRecipientChip) }
|
||||
expandCcAndBccFields()
|
||||
}
|
||||
ccRecipientSection { verify { ccRecipientChip?.let { hasRecipientChips(it) } ?: isEmptyField() } }
|
||||
bccRecipientSection { verify { bccRecipientChip?.let { hasRecipientChips(it) } ?: isEmptyField() } }
|
||||
|
||||
if (ccRecipientChip != null && bccRecipientChip != null) {
|
||||
toRecipientSection { verify { chevronNotVisible() } }
|
||||
ccRecipientSection { verify { hasRecipientChips(ccRecipientChip) } }
|
||||
bccRecipientSection { verify { hasRecipientChips(bccRecipientChip) } }
|
||||
} else {
|
||||
toRecipientSection { expandCcAndBccFields() }
|
||||
ccRecipientSection { verify { isEmptyField() } }
|
||||
bccRecipientSection { verify { isEmptyField() } }
|
||||
}
|
||||
|
||||
subjectSection { verify { hasSubject(subject) } }
|
||||
messageBodySection { verify { messageBody?.let { hasText(it) } ?: hasPlaceholderText() } }
|
||||
}
|
||||
|
||||
+8
-2
@@ -33,10 +33,11 @@ import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import ch.protonmail.android.mailcomposer.presentation.ui.ComposerTestTags
|
||||
import ch.protonmail.android.test.utils.ComposeTestRuleHolder
|
||||
import ch.protonmail.android.uicomponents.chips.ChipsTestTags
|
||||
import ch.protonmail.android.uitest.robot.composer.model.chips.RecipientChipEntry
|
||||
import ch.protonmail.android.uitest.robot.composer.model.chips.RecipientChipEntryModel
|
||||
import ch.protonmail.android.test.utils.ComposeTestRuleHolder
|
||||
import ch.protonmail.android.uitest.robot.composer.model.chips.RecipientChipValidationState.Invalid
|
||||
import ch.protonmail.android.uitest.util.assertions.assertEmptyText
|
||||
import ch.protonmail.android.uitest.util.awaitHidden
|
||||
|
||||
@@ -110,7 +111,12 @@ internal sealed class ComposerRecipientsEntryModel(
|
||||
model
|
||||
.hasText(chip.text)
|
||||
.hasEmailValidationState(chip.state)
|
||||
.also { if (chip.hasDeleteIcon) model.hasDeleteIcon() else model.hasNoDeleteIcon() }
|
||||
.also {
|
||||
if (chip.hasDeleteIcon) model.hasDeleteIcon() else model.hasNoDeleteIcon()
|
||||
}
|
||||
.also {
|
||||
if (chip.state is Invalid) model.hasErrorIcon() else model.hasNoErrorIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -26,8 +26,8 @@ import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import ch.protonmail.android.uicomponents.chips.ChipsTestTags
|
||||
import ch.protonmail.android.test.utils.ComposeTestRuleHolder
|
||||
import ch.protonmail.android.uicomponents.chips.ChipsTestTags
|
||||
import ch.protonmail.android.uitest.util.assertions.CustomSemanticsPropertyKeyNames
|
||||
import ch.protonmail.android.uitest.util.child
|
||||
import ch.protonmail.android.uitest.util.extensions.getKeyValueByName
|
||||
@@ -44,7 +44,8 @@ internal class RecipientChipEntryModel(
|
||||
useUnmergedTree = true
|
||||
)
|
||||
private val text = parent.child { hasTestTag(ChipsTestTags.InputChipText) }
|
||||
private val deleteIcon = parent.child { hasTestTag(ChipsTestTags.InputChipIcon) }
|
||||
private val errorIcon = parent.child { hasTestTag(ChipsTestTags.InputChipLeadingIcon) }
|
||||
private val deleteIcon = parent.child { hasTestTag(ChipsTestTags.InputChipTrailingIcon) }
|
||||
|
||||
// region actions
|
||||
fun tapDeleteIcon() = withParentDisplayed {
|
||||
@@ -69,6 +70,14 @@ internal class RecipientChipEntryModel(
|
||||
deleteIcon.assertDoesNotExist()
|
||||
}
|
||||
|
||||
fun hasErrorIcon() = withParentDisplayed {
|
||||
errorIcon.assertExists()
|
||||
}
|
||||
|
||||
fun hasNoErrorIcon() = withParentDisplayed {
|
||||
errorIcon.assertDoesNotExist()
|
||||
}
|
||||
|
||||
fun doesNotExist() {
|
||||
parent.assertDoesNotExist()
|
||||
}
|
||||
|
||||
+2
-2
@@ -19,6 +19,6 @@
|
||||
package ch.protonmail.android.uitest.robot.composer.model.chips
|
||||
|
||||
internal sealed class RecipientChipValidationState(val value: Boolean) {
|
||||
object Valid : RecipientChipValidationState(true)
|
||||
object Invalid : RecipientChipValidationState(false)
|
||||
data object Valid : RecipientChipValidationState(true)
|
||||
data object Invalid : RecipientChipValidationState(false)
|
||||
}
|
||||
|
||||
+8
-16
@@ -25,43 +25,35 @@ import ch.protonmail.android.uitest.util.getTestString
|
||||
|
||||
internal sealed class ComposerSnackbar(value: String, type: SnackbarType) : SnackbarEntry(value, type) {
|
||||
|
||||
object AttachmentUploadError : ComposerSnackbar(
|
||||
data object AttachmentUploadError : ComposerSnackbar(
|
||||
getTestString(R.string.test_mailbox_attachment_uploading_error), SnackbarType.Error
|
||||
)
|
||||
|
||||
object DraftSaved : ComposerSnackbar(
|
||||
data object DraftSaved : ComposerSnackbar(
|
||||
getTestString(R.string.test_mailbox_draft_saved), SnackbarType.Success
|
||||
)
|
||||
|
||||
object DraftOutOfSync : ComposerSnackbar(
|
||||
data object DraftOutOfSync : ComposerSnackbar(
|
||||
getTestString(R.string.test_composer_error_loading_draft), SnackbarType.Default
|
||||
)
|
||||
|
||||
class DuplicateEmailAddress(recipient: String) : ComposerSnackbar(
|
||||
getTestString(R.string.test_composer_error_duplicate_email, recipient), SnackbarType.Default
|
||||
)
|
||||
|
||||
object InvalidEmailAddress : ComposerSnackbar(
|
||||
getTestString(R.string.test_composer_error_invalid_email), SnackbarType.Default
|
||||
)
|
||||
|
||||
object MessageSent : ComposerSnackbar(
|
||||
data object MessageSent : ComposerSnackbar(
|
||||
getTestString(R.string.test_mailbox_message_sending_success), SnackbarType.Success
|
||||
)
|
||||
|
||||
object MessageSentError : ComposerSnackbar(
|
||||
data object MessageSentError : ComposerSnackbar(
|
||||
getTestString(R.string.test_mailbox_message_sending_error), SnackbarType.Error
|
||||
)
|
||||
|
||||
object MessageQueued : ComposerSnackbar(
|
||||
data object MessageQueued : ComposerSnackbar(
|
||||
getTestString(R.string.test_mailbox_message_sending_offline), SnackbarType.Normal
|
||||
)
|
||||
|
||||
object SendingMessage : ComposerSnackbar(
|
||||
data object SendingMessage : ComposerSnackbar(
|
||||
getTestString(R.string.test_mailbox_message_sending), SnackbarType.Normal
|
||||
)
|
||||
|
||||
object UpgradePlanToChangeSender : ComposerSnackbar(
|
||||
data object UpgradePlanToChangeSender : ComposerSnackbar(
|
||||
getTestString(R.string.test_composer_change_sender_paid_feature), SnackbarType.Default
|
||||
)
|
||||
}
|
||||
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.uitest.robot.composer.section
|
||||
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import ch.protonmail.android.mailcomposer.presentation.ui.AddAttachmentsBottomSheetTestTags
|
||||
import ch.protonmail.android.test.ksp.annotations.AttachTo
|
||||
import ch.protonmail.android.test.ksp.annotations.VerifiesOuter
|
||||
import ch.protonmail.android.uitest.robot.ComposeSectionRobot
|
||||
import ch.protonmail.android.uitest.robot.composer.ComposerRobot
|
||||
import ch.protonmail.android.uitest.util.awaitDisplayed
|
||||
import ch.protonmail.android.uitest.util.awaitHidden
|
||||
import ch.protonmail.android.uitest.util.child
|
||||
|
||||
@AttachTo(targets = [ComposerRobot::class], identifier = "attachmentsBottomSheet")
|
||||
internal class ComposerAttachmentsBottomSheetSection : ComposeSectionRobot() {
|
||||
|
||||
private val rootItem = composeTestRule.onNodeWithTag(
|
||||
AddAttachmentsBottomSheetTestTags.RootItem, useUnmergedTree = true
|
||||
)
|
||||
|
||||
private val importRootItem = rootItem.child {
|
||||
hasTestTag(AddAttachmentsBottomSheetTestTags.ImportEntry)
|
||||
}
|
||||
|
||||
private val importIcon = importRootItem.child {
|
||||
hasTestTag(AddAttachmentsBottomSheetTestTags.ImportIcon)
|
||||
}
|
||||
|
||||
private val importText = importRootItem.child {
|
||||
hasTestTag(AddAttachmentsBottomSheetTestTags.ImportText)
|
||||
}
|
||||
|
||||
fun tapImportItem() = withVisibleItem {
|
||||
importText.performClick()
|
||||
}
|
||||
|
||||
@VerifiesOuter
|
||||
inner class Verify {
|
||||
|
||||
fun isShown() {
|
||||
rootItem.awaitDisplayed()
|
||||
}
|
||||
|
||||
// No need for specific entry item models now.
|
||||
fun hasImportOption() = withVisibleItem {
|
||||
importIcon.awaitDisplayed()
|
||||
importText.awaitDisplayed()
|
||||
}
|
||||
|
||||
fun isDismissed() {
|
||||
rootItem.awaitHidden()
|
||||
}
|
||||
}
|
||||
|
||||
private fun withVisibleItem(block: ComposerAttachmentsBottomSheetSection.() -> Unit) {
|
||||
rootItem.awaitDisplayed()
|
||||
block()
|
||||
}
|
||||
}
|
||||
+4
@@ -41,6 +41,10 @@ internal class ComposerRecipientsToSection : ComposerRecipientsSection(
|
||||
useUnmergedTree = true
|
||||
)
|
||||
|
||||
fun chevronNotVisible() = apply {
|
||||
expandRecipientsButton.assertDoesNotExist()
|
||||
}
|
||||
|
||||
fun expandCcAndBccFields() = apply {
|
||||
expandRecipientsButton.performScrollTo().performClick()
|
||||
}
|
||||
|
||||
+2
@@ -23,6 +23,7 @@ import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.test.espresso.Espresso
|
||||
import ch.protonmail.android.mailmessage.presentation.ui.AttachmentFooterTestTags
|
||||
import ch.protonmail.android.test.ksp.annotations.AttachTo
|
||||
import ch.protonmail.android.test.ksp.annotations.VerifiesOuter
|
||||
@@ -74,6 +75,7 @@ internal class MessageFooterAttachmentSection : ComposeSectionRobot() {
|
||||
}
|
||||
|
||||
private fun scrollTo() {
|
||||
Espresso.closeSoftKeyboard()
|
||||
rootItem.awaitDisplayed().performScrollTo()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
package ch.protonmail.android.uitest.rule
|
||||
|
||||
import ch.protonmail.android.mailmailbox.data.local.OnboardingLocalDataSource
|
||||
import ch.protonmail.android.mailmailbox.domain.model.OnboardingPreference
|
||||
import ch.protonmail.android.mailonboarding.data.local.OnboardingLocalDataSource
|
||||
import ch.protonmail.android.mailonboarding.domain.model.OnboardingPreference
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
+5
-7
@@ -40,7 +40,6 @@ import ch.protonmail.android.mailmailbox.presentation.mailbox.model.UnreadFilter
|
||||
import ch.protonmail.android.mailmailbox.presentation.mailbox.model.UpgradeStorageState
|
||||
import ch.protonmail.android.mailmailbox.presentation.mailbox.previewdata.MailboxSearchStateSampleData
|
||||
import ch.protonmail.android.mailmailbox.presentation.mailbox.previewdata.MailboxStateSampleData
|
||||
import ch.protonmail.android.mailonboarding.presentation.model.OnboardingState
|
||||
import ch.protonmail.android.test.annotations.suite.RegressionTest
|
||||
import ch.protonmail.android.testdata.mailbox.MailboxItemUiModelTestData
|
||||
import ch.protonmail.android.testdata.maillabel.MailLabelTestData
|
||||
@@ -95,7 +94,7 @@ internal class MailboxScreenTest : HiltInstrumentedTest() {
|
||||
refreshRequested = false,
|
||||
swipeActions = null,
|
||||
searchState = MailboxSearchStateSampleData.NotSearching,
|
||||
clearState = MailboxListState.Data.ClearState.Hidden
|
||||
clearState = MailboxListState.Data.ClearState.Hidden,
|
||||
)
|
||||
val mailboxState = MailboxStateSampleData.Loading.copy(mailboxListState = mailboxListState)
|
||||
val items = listOf(MailboxItemUiModelTestData.readMailboxItemUiModel)
|
||||
@@ -153,7 +152,7 @@ internal class MailboxScreenTest : HiltInstrumentedTest() {
|
||||
refreshRequested = false,
|
||||
swipeActions = null,
|
||||
searchState = MailboxSearchStateSampleData.NotSearching,
|
||||
clearState = MailboxListState.Data.ClearState.Hidden
|
||||
clearState = MailboxListState.Data.ClearState.Hidden,
|
||||
)
|
||||
val mailboxState = MailboxStateSampleData.Loading.copy(mailboxListState = mailboxListState)
|
||||
val robot = setupScreen(state = mailboxState)
|
||||
@@ -173,7 +172,7 @@ internal class MailboxScreenTest : HiltInstrumentedTest() {
|
||||
refreshRequested = false,
|
||||
swipeActions = null,
|
||||
searchState = MailboxSearchStateSampleData.NotSearching,
|
||||
clearState = MailboxListState.Data.ClearState.Hidden
|
||||
clearState = MailboxListState.Data.ClearState.Hidden,
|
||||
)
|
||||
val mailboxState = MailboxStateSampleData.Loading.copy(mailboxListState = mailboxListState)
|
||||
val robot = setupScreen(state = mailboxState)
|
||||
@@ -207,7 +206,7 @@ internal class MailboxScreenTest : HiltInstrumentedTest() {
|
||||
refreshRequested = false,
|
||||
swipeActions = null,
|
||||
searchState = MailboxSearchStateSampleData.NotSearching,
|
||||
clearState = MailboxListState.Data.ClearState.Hidden
|
||||
clearState = MailboxListState.Data.ClearState.Hidden,
|
||||
),
|
||||
topAppBarState = MailboxTopAppBarState.Data.DefaultMode(
|
||||
currentLabelName = systemLabel.text()
|
||||
@@ -215,14 +214,13 @@ internal class MailboxScreenTest : HiltInstrumentedTest() {
|
||||
upgradeStorageState = UpgradeStorageState(notificationDotVisible = false),
|
||||
unreadFilterState = UnreadFilterState.Loading,
|
||||
bottomAppBarState = BottomBarState.Data.Hidden(emptyList<ActionUiModel>().toImmutableList()),
|
||||
onboardingState = OnboardingState.Hidden,
|
||||
actionResult = Effect.empty(),
|
||||
deleteDialogState = DeleteDialogState.Hidden,
|
||||
deleteAllDialogState = DeleteDialogState.Hidden,
|
||||
storageLimitState = StorageLimitState.HasEnoughSpace,
|
||||
bottomSheetState = null,
|
||||
error = Effect.empty(),
|
||||
showRatingBooster = Effect.empty()
|
||||
showRatingBooster = Effect.empty(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,5 +23,5 @@ object Config {
|
||||
const val targetSdk = 34
|
||||
const val testInstrumentationRunner = "ch.protonmail.android.uitest.HiltTestRunner"
|
||||
const val versionCode = 1
|
||||
const val versionName = "4.0.20"
|
||||
const val versionName = "4.2.0"
|
||||
}
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ class FocusableFormScope<FocusedField> @OptIn(ExperimentalFoundationApi::class)
|
||||
} else {
|
||||
this
|
||||
}.onFocusChanged {
|
||||
if (it.isFocused) onFieldFocused(fieldType)
|
||||
if (it.hasFocus || it.isFocused) onFieldFocused(fieldType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -26,6 +26,7 @@ object MailDimens {
|
||||
val ThinBorder = 0.5.dp
|
||||
val DefaultBorder = 1.dp
|
||||
val AvatarBorderLine = 1.5.dp
|
||||
val OnboardingUpsellBestValueBorder = 2.dp
|
||||
|
||||
val TinySpacing = 2.dp
|
||||
|
||||
@@ -120,4 +121,7 @@ object MailDimens {
|
||||
}
|
||||
|
||||
val NarrowScreenWidth = 360.dp
|
||||
|
||||
val PlanSwitcherHeight = 68.dp
|
||||
val OnboardingUpsellButtonHeight = 48.dp
|
||||
}
|
||||
|
||||
+9
-7
@@ -18,18 +18,20 @@
|
||||
|
||||
package ch.protonmail.android.mailcommon.presentation.extension
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.navigation.NavController
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Navigates back by popping the backstack only if the current backstack entry is
|
||||
* in the `RESUMED` lifecycle state.
|
||||
*
|
||||
* This avoids the scenario where tapping the back button twice on the top app bar would trigger
|
||||
* twice the navigation event, causing the app to eventually display a blank screen.
|
||||
* Navigates back by popping the backstack only if the current backstack entry is not the starting destination.
|
||||
* This avoids navigating back to a blank screen if the user taps back/exit too quickly.
|
||||
*/
|
||||
fun NavController.navigateBack() {
|
||||
if (currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED) {
|
||||
val startDestination = graph.startDestinationId
|
||||
|
||||
if (currentDestination?.id != startDestination) {
|
||||
Timber.tag("NavController").d("Navigating back from: ${currentDestination?.route}")
|
||||
popBackStack()
|
||||
} else {
|
||||
Timber.tag("NavController").d("Back navigation ignored, current location: ${currentDestination?.route}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<string name="attachment_download_notification_channel_description">Notificaciones de descargas de archivos adjuntos</string>
|
||||
<string name="email_notification_channel_name">Correos</string>
|
||||
<string name="login_notification_channel_name">Alertas de inicio de sesión</string>
|
||||
<string name="email_notification_channel_description">Notificaciones entrantes de correo electrónico</string>
|
||||
<string name="email_notification_channel_description">Notificaciones de correo electrónico entrante</string>
|
||||
<string name="login_notification_channel_description">Notificaciones de nuevos inicios de sesión</string>
|
||||
<string name="auth_badge_official">Oficial</string>
|
||||
<string name="intent_failure_no_app_found_to_handle_this_action">No se encontró una aplicación para ejecutar esta acción</string>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<string name="action_archive_description">Archivar</string>
|
||||
<string name="action_spam_description">Mover a la carpeta de spam</string>
|
||||
<string name="action_view_in_light_mode_description">Ver el mensaje en modo claro</string>
|
||||
<string name="action_view_in_dark_mode_description">Ver mensaje en modo oscuro</string>
|
||||
<string name="action_view_in_dark_mode_description">Ver el mensaje en modo oscuro</string>
|
||||
<string name="action_print_description">Imprimir</string>
|
||||
<string name="action_view_headers_description">Ver los encabezados</string>
|
||||
<string name="action_view_html_description">Ver el código HTML</string>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<string name="color_copper">コパー</string>
|
||||
<string name="color_sahara">サハラ</string>
|
||||
<string name="color_soil">ソイル</string>
|
||||
<string name="color_slate_blue">Slate blue</string>
|
||||
<string name="color_slate_blue">スレートブルー</string>
|
||||
<string name="color_cobalt">コバルト</string>
|
||||
<string name="color_pacific">パシフィック</string>
|
||||
<string name="color_ocean">オーシャン</string>
|
||||
@@ -114,5 +114,5 @@
|
||||
<string name="color_pickle">ピックル</string>
|
||||
<string name="undo_button_label">取り消し</string>
|
||||
<string name="undo_success_message">操作を取り消しました</string>
|
||||
<string name="undo_failure_message">Action revert failed</string>
|
||||
<string name="undo_failure_message">操作の取り消しに失敗しました</string>
|
||||
</resources>
|
||||
|
||||
+14
@@ -26,17 +26,21 @@ import ch.protonmail.android.composer.data.repository.MessageExpirationTimeRepos
|
||||
import ch.protonmail.android.composer.data.repository.MessagePasswordRepositoryImpl
|
||||
import ch.protonmail.android.composer.data.repository.MessageRepositoryImpl
|
||||
import ch.protonmail.android.mailcomposer.domain.Transactor
|
||||
import ch.protonmail.android.mailcomposer.domain.annotations.NewContactSuggestionsEnabled
|
||||
import ch.protonmail.android.mailcomposer.domain.repository.AttachmentRepository
|
||||
import ch.protonmail.android.mailcomposer.domain.repository.AttachmentStateRepository
|
||||
import ch.protonmail.android.mailcomposer.domain.repository.DraftRepository
|
||||
import ch.protonmail.android.mailcomposer.domain.repository.MessageExpirationTimeRepository
|
||||
import ch.protonmail.android.mailcomposer.domain.repository.MessagePasswordRepository
|
||||
import ch.protonmail.android.mailcomposer.domain.repository.MessageRepository
|
||||
import ch.protonmail.android.mailcomposer.domain.usecase.featureflags.IsNewContactsSuggestionsEnabled
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@@ -72,3 +76,13 @@ abstract class MailComposerModule {
|
||||
impl: MessageExpirationTimeRepositoryImpl
|
||||
): MessageExpirationTimeRepository
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object FeatureFlagModule {
|
||||
|
||||
@Provides
|
||||
@NewContactSuggestionsEnabled
|
||||
@Singleton
|
||||
fun provideNewContactsSuggestionsEnabled(isEnabled: IsNewContactsSuggestionsEnabled) = isEnabled(null)
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailcomposer.domain.annotations
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
/**
|
||||
* Indicates to use the new contact suggestions layout.
|
||||
*/
|
||||
@Qualifier
|
||||
annotation class NewContactSuggestionsEnabled
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailcomposer.domain.usecase.featureflags
|
||||
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.featureflag.domain.ExperimentalProtonFeatureFlag
|
||||
import me.proton.core.featureflag.domain.FeatureFlagManager
|
||||
import me.proton.core.featureflag.domain.entity.FeatureId
|
||||
import javax.inject.Inject
|
||||
|
||||
class IsNewContactsSuggestionsEnabled @Inject constructor(
|
||||
private val featureFlagManager: FeatureFlagManager
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalProtonFeatureFlag::class)
|
||||
operator fun invoke(userId: UserId?) = featureFlagManager.getValue(userId, FeatureId(FeatureFlagId))
|
||||
|
||||
private companion object {
|
||||
|
||||
const val FeatureFlagId = "MailAndroidComposerNewContactsSuggestions"
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -30,6 +30,7 @@ data class ComposerDraftState(
|
||||
val fields: ComposerFields,
|
||||
val attachments: AttachmentGroupUiModel,
|
||||
val premiumFeatureMessage: Effect<TextUiModel>,
|
||||
val recipientValidationError: Effect<TextUiModel>,
|
||||
val error: Effect<TextUiModel>,
|
||||
val isSubmittable: Boolean,
|
||||
val isDeviceContactsSuggestionsEnabled: Boolean,
|
||||
@@ -54,7 +55,8 @@ data class ComposerDraftState(
|
||||
val areContactSuggestionsExpanded: Map<ContactSuggestionsField, Boolean> = emptyMap(),
|
||||
val senderChangedNotice: Effect<TextUiModel> = Effect.empty(),
|
||||
val messageExpiresIn: Duration,
|
||||
val confirmSendExpiringMessage: Effect<List<Participant>>
|
||||
val confirmSendExpiringMessage: Effect<List<Participant>>,
|
||||
val openImagePicker: Effect<Unit>
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -79,6 +81,7 @@ data class ComposerDraftState(
|
||||
attachments = emptyList()
|
||||
),
|
||||
premiumFeatureMessage = Effect.empty(),
|
||||
recipientValidationError = Effect.empty(),
|
||||
error = Effect.empty(),
|
||||
isSubmittable = isSubmittable,
|
||||
senderAddresses = emptyList(),
|
||||
@@ -100,7 +103,8 @@ data class ComposerDraftState(
|
||||
messageExpiresIn = Duration.ZERO,
|
||||
confirmSendExpiringMessage = Effect.empty(),
|
||||
isDeviceContactsSuggestionsEnabled = false,
|
||||
isDeviceContactsSuggestionsPromptEnabled = false
|
||||
isDeviceContactsSuggestionsPromptEnabled = false,
|
||||
openImagePicker = Effect.empty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -52,7 +52,6 @@ internal sealed interface ComposerAction : ComposerOperation {
|
||||
data class RemoveAttachment(val attachmentId: AttachmentId) : ComposerAction
|
||||
|
||||
data object ChangeSenderRequested : ComposerAction
|
||||
data object OnBottomSheetOptionSelected : ComposerAction
|
||||
data object OnAddAttachments : ComposerAction
|
||||
data object OnCloseComposer : ComposerAction
|
||||
data object OnSendMessage : ComposerAction
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ sealed class ContactSuggestionUiModel(
|
||||
|
||||
data class Contact(
|
||||
override val name: String,
|
||||
val initial: String,
|
||||
val email: String
|
||||
) : ContactSuggestionUiModel(name)
|
||||
|
||||
|
||||
+11
-36
@@ -20,7 +20,6 @@ package ch.protonmail.android.mailcomposer.presentation.reducer
|
||||
|
||||
import ch.protonmail.android.mailcommon.presentation.Effect
|
||||
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
|
||||
import ch.protonmail.android.mailmessage.domain.model.DraftAction
|
||||
import ch.protonmail.android.mailcomposer.domain.model.DraftBody
|
||||
import ch.protonmail.android.mailcomposer.domain.model.MessageExpirationTime
|
||||
import ch.protonmail.android.mailcomposer.domain.model.MessagePassword
|
||||
@@ -30,12 +29,13 @@ import ch.protonmail.android.mailcomposer.presentation.model.ComposerAction
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ComposerDraftState
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ComposerEvent
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ComposerOperation
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.DraftUiModel
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.RecipientUiModel
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.SenderUiModel
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ContactSuggestionUiModel
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ContactSuggestionsField
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.DraftUiModel
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.FocusedFieldType
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.RecipientUiModel
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.SenderUiModel
|
||||
import ch.protonmail.android.mailmessage.domain.model.DraftAction
|
||||
import ch.protonmail.android.mailmessage.domain.model.MessageAttachment
|
||||
import ch.protonmail.android.mailmessage.presentation.mapper.AttachmentUiModelMapper
|
||||
import ch.protonmail.android.mailmessage.presentation.model.AttachmentGroupUiModel
|
||||
@@ -66,8 +66,7 @@ class ComposerReducer @Inject constructor(
|
||||
is ComposerAction.ContactSuggestionTermChanged -> currentState
|
||||
is ComposerAction.DraftBodyChanged -> updateDraftBodyTo(currentState, this.draftBody)
|
||||
is ComposerAction.SubjectChanged -> updateSubjectTo(currentState, this.subject)
|
||||
is ComposerAction.OnBottomSheetOptionSelected -> updateBottomSheetVisibility(currentState, false)
|
||||
is ComposerAction.OnAddAttachments -> updateBottomSheetVisibility(currentState, true)
|
||||
is ComposerAction.OnAddAttachments -> updateForOnAddAttachments(currentState)
|
||||
is ComposerAction.OnCloseComposer -> updateCloseComposerState(currentState, false)
|
||||
is ComposerAction.ChangeSenderRequested -> currentState
|
||||
is ComposerAction.OnSendMessage -> updateStateForSendMessage(currentState)
|
||||
@@ -194,9 +193,6 @@ class ComposerReducer @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateBottomSheetVisibility(currentState: ComposerDraftState, bottomSheetVisibility: Boolean) =
|
||||
currentState.copy(changeBottomSheetVisibility = Effect.of(bottomSheetVisibility))
|
||||
|
||||
private fun updateComposerFieldsState(
|
||||
currentState: ComposerDraftState,
|
||||
draftUiModel: DraftUiModel,
|
||||
@@ -336,6 +332,10 @@ class ComposerReducer @Inject constructor(
|
||||
messageExpirationTime: MessageExpirationTime?
|
||||
) = currentState.copy(messageExpiresIn = messageExpirationTime?.expiresIn ?: Duration.ZERO)
|
||||
|
||||
private fun updateForOnAddAttachments(currentState: ComposerDraftState) = currentState.copy(
|
||||
openImagePicker = Effect.of(Unit)
|
||||
)
|
||||
|
||||
private fun updateRecipientsTo(
|
||||
currentState: ComposerDraftState,
|
||||
recipients: List<RecipientUiModel>
|
||||
@@ -405,15 +405,7 @@ class ComposerReducer @Inject constructor(
|
||||
val hasDuplicates = hasDuplicates(capturedToDuplicates, capturedCcDuplicates, capturedBccDuplicates)
|
||||
|
||||
val error = when {
|
||||
hasDuplicates -> {
|
||||
Effect.of(
|
||||
TextUiModel(
|
||||
R.string.composer_error_duplicate_recipient,
|
||||
getDuplicateEmailsError(capturedToDuplicates, capturedCcDuplicates, capturedBccDuplicates)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
hasDuplicates -> { Effect.of(TextUiModel(R.string.composer_error_duplicate_recipient)) }
|
||||
hasInvalidRecipients -> Effect.of(TextUiModel(R.string.composer_error_invalid_email))
|
||||
else -> Effect.empty()
|
||||
}
|
||||
@@ -424,7 +416,7 @@ class ComposerReducer @Inject constructor(
|
||||
cc = capturedCcDuplicates.cleanedRecipients,
|
||||
bcc = capturedBccDuplicates.cleanedRecipients
|
||||
),
|
||||
error = error,
|
||||
recipientValidationError = error,
|
||||
isSubmittable = allValid && notEmpty
|
||||
)
|
||||
}
|
||||
@@ -437,23 +429,6 @@ class ComposerReducer @Inject constructor(
|
||||
capturedCcDuplicates.duplicatesFound.isNotEmpty() ||
|
||||
capturedBccDuplicates.duplicatesFound.isNotEmpty()
|
||||
|
||||
private fun getDuplicateEmailsError(
|
||||
capturedToDuplicates: CleanedRecipients,
|
||||
capturedCcDuplicates: CleanedRecipients,
|
||||
capturedBccDuplicates: CleanedRecipients
|
||||
): String {
|
||||
val duplicates = capturedToDuplicates.duplicatesFound +
|
||||
capturedCcDuplicates.duplicatesFound +
|
||||
capturedBccDuplicates.duplicatesFound
|
||||
|
||||
val validDuplicates = duplicates.filterIsInstance<RecipientUiModel.Valid>()
|
||||
val inValidDuplicates = duplicates.filterIsInstance<RecipientUiModel.Invalid>()
|
||||
|
||||
return (validDuplicates.map { it.address } + inValidDuplicates.map { it.address })
|
||||
.distinct()
|
||||
.joinToString(", ")
|
||||
}
|
||||
|
||||
private fun captureDuplicateEmails(recipients: List<RecipientUiModel>): CleanedRecipients {
|
||||
val itemsCounted = recipients.groupingBy { it }.eachCount()
|
||||
return CleanedRecipients(
|
||||
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailcomposer.presentation.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import ch.protonmail.android.mailcommon.presentation.NO_CONTENT_DESCRIPTION
|
||||
import ch.protonmail.android.mailcomposer.presentation.R
|
||||
import me.proton.core.compose.component.ProtonRawListItem
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
|
||||
@Composable
|
||||
fun AddAttachmentsBottomSheetContent(onImportFromSelected: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.testTag(AddAttachmentsBottomSheetTestTags.RootItem)
|
||||
.padding(vertical = ProtonDimens.DefaultSpacing)
|
||||
) {
|
||||
ProtonRawListItem(
|
||||
modifier = Modifier
|
||||
.testTag(AddAttachmentsBottomSheetTestTags.ImportEntry)
|
||||
.clickable { onImportFromSelected() }
|
||||
.height(ProtonDimens.ListItemHeight)
|
||||
.padding(horizontal = ProtonDimens.DefaultSpacing)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.testTag(AddAttachmentsBottomSheetTestTags.ImportIcon),
|
||||
painter = painterResource(id = R.drawable.ic_proton_folder_open),
|
||||
contentDescription = NO_CONTENT_DESCRIPTION
|
||||
)
|
||||
Spacer(modifier = Modifier.width(ProtonDimens.DefaultSpacing))
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.testTag(AddAttachmentsBottomSheetTestTags.ImportText)
|
||||
.weight(1f),
|
||||
text = stringResource(id = R.string.composer_add_attachments_bottom_sheet_import_from),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AddAttachmentsBottomSheetTestTags {
|
||||
|
||||
const val RootItem = "AttachmentsBottomSheetRootItem"
|
||||
const val ImportEntry = "ImportEntry"
|
||||
const val ImportIcon = "ImportIcon"
|
||||
const val ImportText = "ImportText"
|
||||
}
|
||||
+60
-210
@@ -18,57 +18,33 @@
|
||||
|
||||
package ch.protonmail.android.mailcomposer.presentation.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import ch.protonmail.android.mailcommon.presentation.ConsumableLaunchedEffect
|
||||
import ch.protonmail.android.mailcommon.presentation.Effect
|
||||
import ch.protonmail.android.mailcommon.presentation.compose.FocusableForm
|
||||
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
|
||||
import ch.protonmail.android.mailcommon.presentation.model.string
|
||||
import ch.protonmail.android.mailcommon.presentation.ui.MailDivider
|
||||
import ch.protonmail.android.mailcomposer.presentation.R
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ComposerFields
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ContactSuggestionUiModel
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ContactSuggestionsField
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.FocusedFieldType
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.RecipientUiModel
|
||||
import ch.protonmail.android.uicomponents.chips.ChipItem
|
||||
import ch.protonmail.android.uicomponents.chips.ChipsListField
|
||||
import ch.protonmail.android.uicomponents.chips.ContactSuggestionItem
|
||||
import ch.protonmail.android.uicomponents.chips.ContactSuggestionState
|
||||
import ch.protonmail.android.uicomponents.chips.thenIf
|
||||
import ch.protonmail.android.uicomponents.keyboardVisibilityAsState
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
internal fun ComposerForm(
|
||||
newContactSuggestionsEnabled: Boolean,
|
||||
emailValidator: (String) -> Boolean,
|
||||
recipientsOpen: Boolean,
|
||||
initialFocus: FocusedFieldType,
|
||||
@@ -83,13 +59,18 @@ internal fun ComposerForm(
|
||||
) {
|
||||
val isKeyboardVisible by keyboardVisibilityAsState()
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val maxWidthModifier = Modifier.fillMaxWidth()
|
||||
val emailNextKeyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Email
|
||||
)
|
||||
val recipientsButtonRotation = remember { Animatable(0F) }
|
||||
|
||||
var showSubjectAndBody by remember { mutableStateOf(true) }
|
||||
|
||||
// Handle visibility of body and subject here, to avoid issues with focus requesters.
|
||||
LaunchedEffect(areContactSuggestionsExpanded, newContactSuggestionsEnabled) {
|
||||
showSubjectAndBody = if (newContactSuggestionsEnabled) {
|
||||
!areContactSuggestionsExpanded.any { it.value }
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
FocusableForm(
|
||||
fieldList = listOf(
|
||||
@@ -123,188 +104,57 @@ internal fun ComposerForm(
|
||||
actions.onChangeSender
|
||||
)
|
||||
MailDivider()
|
||||
Row(
|
||||
modifier = maxWidthModifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
ChipsListField(
|
||||
label = stringResource(id = R.string.to_prefix),
|
||||
value = fields.to.map { it.toChipItem() },
|
||||
chipValidator = emailValidator,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = ProtonDimens.DefaultSpacing)
|
||||
.testTag(ComposerTestTags.ToRecipient)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.TO),
|
||||
keyboardOptions = emailNextKeyboardOptions,
|
||||
focusRequester = fieldFocusRequesters[FocusedFieldType.TO],
|
||||
actions = ChipsListField.Actions(
|
||||
onSuggestionTermTyped = {
|
||||
actions.onContactSuggestionTermChanged(it, ContactSuggestionsField.TO)
|
||||
},
|
||||
onSuggestionsDismissed = { actions.onContactSuggestionsDismissed(ContactSuggestionsField.TO) },
|
||||
onListChanged = {
|
||||
actions.onToChanged(it.mapNotNull { chipItem -> chipItem.toRecipientUiModel() })
|
||||
}
|
||||
),
|
||||
contactSuggestionState = ContactSuggestionState(
|
||||
areSuggestionsExpanded = areContactSuggestionsExpanded[ContactSuggestionsField.TO] ?: false,
|
||||
contactSuggestionItems = contactSuggestions[ContactSuggestionsField.TO]?.map {
|
||||
it.toSuggestionContactItem()
|
||||
} ?: emptyList()
|
||||
)
|
||||
|
||||
if (newContactSuggestionsEnabled) {
|
||||
RecipientFields2(
|
||||
fields = fields,
|
||||
fieldFocusRequesters = fieldFocusRequesters,
|
||||
recipientsOpen = recipientsOpen,
|
||||
emailValidator = emailValidator,
|
||||
contactSuggestions = contactSuggestions,
|
||||
areContactSuggestionsExpanded = areContactSuggestionsExpanded,
|
||||
actions = actions
|
||||
)
|
||||
Spacer(modifier = Modifier.size(ProtonDimens.DefaultSpacing))
|
||||
IconButton(
|
||||
modifier = Modifier.focusProperties { canFocus = false },
|
||||
onClick = { actions.onToggleRecipients(!recipientsOpen) }
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.thenIf(recipientsButtonRotation.value == RecipientsButtonRotationValues.Closed) {
|
||||
testTag(ComposerTestTags.ExpandCollapseArrow)
|
||||
}
|
||||
.thenIf(recipientsButtonRotation.value == RecipientsButtonRotationValues.Open) {
|
||||
testTag(ComposerTestTags.CollapseExpandArrow)
|
||||
}
|
||||
.rotate(recipientsButtonRotation.value),
|
||||
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||
tint = ProtonTheme.colors.textWeak,
|
||||
contentDescription = stringResource(id = R.string.composer_expand_recipients_button)
|
||||
} else {
|
||||
RecipientFields(
|
||||
fields = fields,
|
||||
fieldFocusRequesters = fieldFocusRequesters,
|
||||
recipientsOpen = recipientsOpen,
|
||||
emailValidator = emailValidator,
|
||||
contactSuggestions = contactSuggestions,
|
||||
areContactSuggestionsExpanded = areContactSuggestionsExpanded,
|
||||
actions = actions
|
||||
)
|
||||
}
|
||||
|
||||
if (showSubjectAndBody) {
|
||||
MailDivider()
|
||||
SubjectTextField(
|
||||
initialValue = fields.subject,
|
||||
onSubjectChange = actions.onSubjectChanged,
|
||||
modifier = maxWidthModifier
|
||||
.testTag(ComposerTestTags.Subject)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.SUBJECT)
|
||||
)
|
||||
MailDivider()
|
||||
BodyTextField(
|
||||
initialValue = fields.body,
|
||||
shouldRequestFocus = shouldForceBodyTextFocus,
|
||||
replaceDraftBody = replaceDraftBody,
|
||||
onBodyChange = actions.onBodyChanged,
|
||||
modifier = maxWidthModifier
|
||||
.testTag(ComposerTestTags.MessageBody)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.BODY)
|
||||
)
|
||||
if (fields.quotedBody != null) {
|
||||
RespondInlineButton(actions.onRespondInline)
|
||||
BodyHtmlQuote(
|
||||
value = fields.quotedBody.styled.value,
|
||||
modifier = maxWidthModifier.testTag(ComposerTestTags.MessageHtmlQuotedBody)
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = recipientsOpen,
|
||||
modifier = Modifier.animateContentSize()
|
||||
) {
|
||||
Column {
|
||||
MailDivider()
|
||||
ChipsListField(
|
||||
label = stringResource(id = R.string.cc_prefix),
|
||||
value = fields.cc.map { it.toChipItem() },
|
||||
chipValidator = emailValidator,
|
||||
modifier = Modifier
|
||||
.padding(start = ProtonDimens.DefaultSpacing)
|
||||
.testTag(ComposerTestTags.CcRecipient)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.CC),
|
||||
keyboardOptions = emailNextKeyboardOptions,
|
||||
focusRequester = fieldFocusRequesters[FocusedFieldType.CC],
|
||||
actions = ChipsListField.Actions(
|
||||
onSuggestionTermTyped = {
|
||||
actions.onContactSuggestionTermChanged(it, ContactSuggestionsField.CC)
|
||||
},
|
||||
onSuggestionsDismissed = {
|
||||
actions.onContactSuggestionsDismissed(ContactSuggestionsField.CC)
|
||||
},
|
||||
onListChanged = {
|
||||
actions.onCcChanged(it.mapNotNull { chipItem -> chipItem.toRecipientUiModel() })
|
||||
}
|
||||
),
|
||||
contactSuggestionState = ContactSuggestionState(
|
||||
areSuggestionsExpanded = areContactSuggestionsExpanded[ContactSuggestionsField.CC] ?: false,
|
||||
contactSuggestionItems = contactSuggestions[ContactSuggestionsField.CC]?.map {
|
||||
it.toSuggestionContactItem()
|
||||
} ?: emptyList()
|
||||
)
|
||||
)
|
||||
MailDivider()
|
||||
ChipsListField(
|
||||
label = stringResource(id = R.string.bcc_prefix),
|
||||
value = fields.bcc.map { it.toChipItem() },
|
||||
chipValidator = emailValidator,
|
||||
modifier = Modifier
|
||||
.padding(start = ProtonDimens.DefaultSpacing)
|
||||
.testTag(ComposerTestTags.BccRecipient)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.BCC),
|
||||
keyboardOptions = emailNextKeyboardOptions,
|
||||
focusRequester = fieldFocusRequesters[FocusedFieldType.BCC],
|
||||
actions = ChipsListField.Actions(
|
||||
onSuggestionTermTyped = {
|
||||
actions.onContactSuggestionTermChanged(it, ContactSuggestionsField.BCC)
|
||||
},
|
||||
onSuggestionsDismissed = {
|
||||
actions.onContactSuggestionsDismissed(ContactSuggestionsField.BCC)
|
||||
},
|
||||
onListChanged = {
|
||||
actions.onBccChanged(it.mapNotNull { chipItem -> chipItem.toRecipientUiModel() })
|
||||
}
|
||||
),
|
||||
contactSuggestionState = ContactSuggestionState(
|
||||
areSuggestionsExpanded = areContactSuggestionsExpanded[ContactSuggestionsField.BCC]
|
||||
?: false,
|
||||
contactSuggestionItems = contactSuggestions[ContactSuggestionsField.BCC]?.map {
|
||||
it.toSuggestionContactItem()
|
||||
} ?: emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
MailDivider()
|
||||
SubjectTextField(
|
||||
initialValue = fields.subject,
|
||||
onSubjectChange = actions.onSubjectChanged,
|
||||
modifier = maxWidthModifier
|
||||
.testTag(ComposerTestTags.Subject)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.SUBJECT)
|
||||
)
|
||||
MailDivider()
|
||||
BodyTextField(
|
||||
initialValue = fields.body,
|
||||
shouldRequestFocus = shouldForceBodyTextFocus,
|
||||
replaceDraftBody = replaceDraftBody,
|
||||
onBodyChange = actions.onBodyChanged,
|
||||
modifier = maxWidthModifier
|
||||
.testTag(ComposerTestTags.MessageBody)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.BODY)
|
||||
)
|
||||
if (fields.quotedBody != null) {
|
||||
RespondInlineButton(actions.onRespondInline)
|
||||
BodyHtmlQuote(
|
||||
value = fields.quotedBody.styled.value,
|
||||
modifier = maxWidthModifier.testTag(ComposerTestTags.MessageHtmlQuotedBody)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = recipientsOpen) {
|
||||
recipientsButtonRotation.animateTo(
|
||||
if (recipientsOpen) RecipientsButtonRotationValues.Open else RecipientsButtonRotationValues.Closed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object RecipientsButtonRotationValues {
|
||||
|
||||
const val Open = 180f
|
||||
const val Closed = 0f
|
||||
}
|
||||
|
||||
private fun ChipItem.toRecipientUiModel(): RecipientUiModel? = when (this) {
|
||||
is ChipItem.Counter -> null
|
||||
is ChipItem.Invalid -> RecipientUiModel.Invalid(value)
|
||||
is ChipItem.Valid -> RecipientUiModel.Valid(value)
|
||||
}
|
||||
|
||||
private fun RecipientUiModel.toChipItem(): ChipItem = when (this) {
|
||||
is RecipientUiModel.Invalid -> ChipItem.Invalid(address)
|
||||
is RecipientUiModel.Valid -> ChipItem.Valid(address)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactSuggestionUiModel.toSuggestionContactItem(): ContactSuggestionItem = when (this) {
|
||||
is ContactSuggestionUiModel.Contact -> ContactSuggestionItem(
|
||||
this.name,
|
||||
this.email,
|
||||
listOf(this.email)
|
||||
)
|
||||
is ContactSuggestionUiModel.ContactGroup -> ContactSuggestionItem(
|
||||
this.name,
|
||||
TextUiModel.PluralisedText(
|
||||
value = R.plurals.composer_recipient_suggestion_contacts,
|
||||
count = this.emails.size
|
||||
).string(),
|
||||
this.emails
|
||||
)
|
||||
}
|
||||
|
||||
+13
-11
@@ -20,6 +20,7 @@ package ch.protonmail.android.mailcomposer.presentation.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.text.format.Formatter
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -97,7 +98,7 @@ fun ComposerScreen(actions: ComposerScreen.Actions, viewModel: ComposerViewModel
|
||||
mutableStateOf(if (state.fields.to.isEmpty()) FocusedFieldType.TO else FocusedFieldType.BODY)
|
||||
}
|
||||
val snackbarHostState = remember { ProtonSnackbarHostState() }
|
||||
val bottomSheetType = rememberSaveable { mutableStateOf(BottomSheetType.AddAttachments) }
|
||||
val bottomSheetType = rememberSaveable { mutableStateOf(BottomSheetType.ChangeSender) }
|
||||
val bottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val attachmentSizeDialogState = remember { mutableStateOf(false) }
|
||||
val sendingErrorDialogState = remember { mutableStateOf<String?>(null) }
|
||||
@@ -151,16 +152,13 @@ fun ComposerScreen(actions: ComposerScreen.Actions, viewModel: ComposerViewModel
|
||||
}
|
||||
}
|
||||
|
||||
ConsumableLaunchedEffect(effect = state.openImagePicker) {
|
||||
imagePicker.launch("*/*")
|
||||
}
|
||||
|
||||
ProtonModalBottomSheetLayout(
|
||||
sheetContent = bottomSheetHeightConstrainedContent {
|
||||
when (bottomSheetType.value) {
|
||||
BottomSheetType.AddAttachments -> AddAttachmentsBottomSheetContent(
|
||||
onImportFromSelected = {
|
||||
viewModel.submit(ComposerAction.OnBottomSheetOptionSelected)
|
||||
imagePicker.launch("*/*")
|
||||
}
|
||||
)
|
||||
|
||||
BottomSheetType.ChangeSender -> ChangeSenderBottomSheetContent(
|
||||
state.senderAddresses,
|
||||
{ sender -> viewModel.submit(ComposerAction.SenderChanged(sender)) }
|
||||
@@ -180,7 +178,6 @@ fun ComposerScreen(actions: ComposerScreen.Actions, viewModel: ComposerViewModel
|
||||
ComposerTopBar(
|
||||
attachmentsCount = state.attachments.attachments.size,
|
||||
onAddAttachmentsClick = {
|
||||
bottomSheetType.value = BottomSheetType.AddAttachments
|
||||
viewModel.submit(ComposerAction.OnAddAttachments)
|
||||
},
|
||||
onCloseComposerClick = {
|
||||
@@ -245,7 +242,8 @@ fun ComposerScreen(actions: ComposerScreen.Actions, viewModel: ComposerViewModel
|
||||
{ bottomSheetType.value = it }
|
||||
),
|
||||
contactSuggestions = state.contactSuggestions,
|
||||
areContactSuggestionsExpanded = state.areContactSuggestionsExpanded
|
||||
areContactSuggestionsExpanded = state.areContactSuggestionsExpanded,
|
||||
newContactSuggestionsEnabled = viewModel.newContactSuggestionsEnabled
|
||||
)
|
||||
if (state.attachments.attachments.isNotEmpty()) {
|
||||
AttachmentFooter(
|
||||
@@ -335,6 +333,10 @@ fun ComposerScreen(actions: ComposerScreen.Actions, viewModel: ComposerViewModel
|
||||
)
|
||||
}
|
||||
|
||||
ConsumableTextEffect(effect = state.recipientValidationError) { error ->
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
ConsumableTextEffect(effect = state.premiumFeatureMessage) { message ->
|
||||
snackbarHostState.showSnackbar(type = ProtonSnackbarType.NORM, message = message)
|
||||
}
|
||||
@@ -473,7 +475,7 @@ object ComposerScreen {
|
||||
}
|
||||
}
|
||||
|
||||
private enum class BottomSheetType { AddAttachments, ChangeSender, SetExpirationTime }
|
||||
private enum class BottomSheetType { ChangeSender, SetExpirationTime }
|
||||
|
||||
private data class SendExpiringMessageDialogState(
|
||||
val isVisible: Boolean,
|
||||
|
||||
+11
-15
@@ -19,13 +19,12 @@
|
||||
package ch.protonmail.android.mailcomposer.presentation.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
@@ -35,7 +34,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import ch.protonmail.android.mailcommon.presentation.NO_CONTENT_DESCRIPTION
|
||||
@@ -87,19 +85,17 @@ internal fun PrefixedEmailSelector(
|
||||
|
||||
@Composable
|
||||
private fun ChangeSenderButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
Icon(
|
||||
IconButton(
|
||||
modifier = modifier
|
||||
.padding(horizontal = ProtonDimens.MediumSpacing)
|
||||
.testTag(ComposerTestTags.ChangeSenderButton)
|
||||
.clickable(
|
||||
onClickLabel = stringResource(id = R.string.change_sender_button_content_description),
|
||||
role = Role.Button,
|
||||
onClick = onClick
|
||||
),
|
||||
painter = painterResource(id = R.drawable.ic_proton_three_dots_vertical),
|
||||
tint = ProtonTheme.colors.iconWeak,
|
||||
contentDescription = NO_CONTENT_DESCRIPTION
|
||||
)
|
||||
.testTag(ComposerTestTags.ChangeSenderButton),
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_proton_three_dots_vertical),
|
||||
tint = ProtonTheme.colors.iconWeak,
|
||||
contentDescription = NO_CONTENT_DESCRIPTION
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object PrefixedEmailSelectorTestTags {
|
||||
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailcomposer.presentation.ui
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import ch.protonmail.android.mailcommon.presentation.compose.FocusableFormScope
|
||||
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
|
||||
import ch.protonmail.android.mailcommon.presentation.model.string
|
||||
import ch.protonmail.android.mailcommon.presentation.ui.MailDivider
|
||||
import ch.protonmail.android.mailcomposer.presentation.R
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ComposerFields
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ContactSuggestionUiModel
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ContactSuggestionsField
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.FocusedFieldType
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.RecipientUiModel
|
||||
import ch.protonmail.android.uicomponents.chips.ChipsListField
|
||||
import ch.protonmail.android.uicomponents.chips.ContactSuggestionItem
|
||||
import ch.protonmail.android.uicomponents.chips.ContactSuggestionState
|
||||
import ch.protonmail.android.uicomponents.chips.item.ChipItem
|
||||
import ch.protonmail.android.uicomponents.chips.thenIf
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
|
||||
@Composable
|
||||
internal fun FocusableFormScope<FocusedFieldType>.RecipientFields(
|
||||
modifier: Modifier = Modifier,
|
||||
fields: ComposerFields,
|
||||
fieldFocusRequesters: Map<FocusedFieldType, FocusRequester>,
|
||||
recipientsOpen: Boolean,
|
||||
emailValidator: (String) -> Boolean,
|
||||
contactSuggestions: Map<ContactSuggestionsField, List<ContactSuggestionUiModel>>,
|
||||
actions: ComposerFormActions,
|
||||
areContactSuggestionsExpanded: Map<ContactSuggestionsField, Boolean>
|
||||
) {
|
||||
val emailNextKeyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Email
|
||||
)
|
||||
val recipientsButtonRotation = remember { Animatable(0F) }
|
||||
val hasCcBccContent = fields.cc.isNotEmpty() || fields.bcc.isNotEmpty()
|
||||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
ChipsListField(
|
||||
label = stringResource(id = R.string.to_prefix),
|
||||
value = fields.to.map { it.toChipItem() },
|
||||
chipValidator = emailValidator,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = ProtonDimens.DefaultSpacing)
|
||||
.testTag(ComposerTestTags.ToRecipient)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.TO),
|
||||
keyboardOptions = emailNextKeyboardOptions,
|
||||
focusRequester = fieldFocusRequesters[FocusedFieldType.TO],
|
||||
actions = ChipsListField.Actions(
|
||||
onSuggestionTermTyped = {
|
||||
actions.onContactSuggestionTermChanged(it, ContactSuggestionsField.TO)
|
||||
},
|
||||
onSuggestionsDismissed = { actions.onContactSuggestionsDismissed(ContactSuggestionsField.TO) },
|
||||
onListChanged = {
|
||||
actions.onToChanged(it.mapNotNull { chipItem -> chipItem.toRecipientUiModel() })
|
||||
}
|
||||
),
|
||||
contactSuggestionState = ContactSuggestionState(
|
||||
areSuggestionsExpanded = areContactSuggestionsExpanded[ContactSuggestionsField.TO] ?: false,
|
||||
contactSuggestionItems = contactSuggestions[ContactSuggestionsField.TO]?.map {
|
||||
it.toSuggestionContactItem()
|
||||
} ?: emptyList()
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(ProtonDimens.DefaultSpacing))
|
||||
if (!hasCcBccContent) {
|
||||
IconButton(
|
||||
modifier = Modifier.focusProperties { canFocus = false },
|
||||
onClick = { actions.onToggleRecipients(!recipientsOpen) }
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.thenIf(recipientsButtonRotation.value == RecipientsButtonRotationValues.Closed) {
|
||||
testTag(ComposerTestTags.ExpandCollapseArrow)
|
||||
}
|
||||
.thenIf(recipientsButtonRotation.value == RecipientsButtonRotationValues.Open) {
|
||||
testTag(ComposerTestTags.CollapseExpandArrow)
|
||||
}
|
||||
.rotate(recipientsButtonRotation.value)
|
||||
.size(ProtonDimens.SmallIconSize),
|
||||
imageVector = ImageVector.vectorResource(
|
||||
id = me.proton.core.presentation.R.drawable.ic_proton_chevron_down_filled
|
||||
),
|
||||
tint = ProtonTheme.colors.textWeak,
|
||||
contentDescription = stringResource(id = R.string.composer_expand_recipients_button)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientsOpen || hasCcBccContent) {
|
||||
Column {
|
||||
MailDivider()
|
||||
ChipsListField(
|
||||
label = stringResource(id = R.string.cc_prefix),
|
||||
value = fields.cc.map { it.toChipItem() },
|
||||
chipValidator = emailValidator,
|
||||
modifier = Modifier
|
||||
.padding(start = ProtonDimens.DefaultSpacing)
|
||||
.testTag(ComposerTestTags.CcRecipient)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.CC),
|
||||
keyboardOptions = emailNextKeyboardOptions,
|
||||
focusRequester = fieldFocusRequesters[FocusedFieldType.CC],
|
||||
actions = ChipsListField.Actions(
|
||||
onSuggestionTermTyped = {
|
||||
actions.onContactSuggestionTermChanged(it, ContactSuggestionsField.CC)
|
||||
},
|
||||
onSuggestionsDismissed = {
|
||||
actions.onContactSuggestionsDismissed(ContactSuggestionsField.CC)
|
||||
},
|
||||
onListChanged = {
|
||||
actions.onCcChanged(it.mapNotNull { chipItem -> chipItem.toRecipientUiModel() })
|
||||
}
|
||||
),
|
||||
contactSuggestionState = ContactSuggestionState(
|
||||
areSuggestionsExpanded = areContactSuggestionsExpanded[ContactSuggestionsField.CC] ?: false,
|
||||
contactSuggestionItems = contactSuggestions[ContactSuggestionsField.CC]?.map {
|
||||
it.toSuggestionContactItem()
|
||||
} ?: emptyList()
|
||||
)
|
||||
)
|
||||
MailDivider()
|
||||
ChipsListField(
|
||||
label = stringResource(id = R.string.bcc_prefix),
|
||||
value = fields.bcc.map { it.toChipItem() },
|
||||
chipValidator = emailValidator,
|
||||
modifier = Modifier
|
||||
.padding(start = ProtonDimens.DefaultSpacing)
|
||||
.testTag(ComposerTestTags.BccRecipient)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.BCC),
|
||||
keyboardOptions = emailNextKeyboardOptions,
|
||||
focusRequester = fieldFocusRequesters[FocusedFieldType.BCC],
|
||||
actions = ChipsListField.Actions(
|
||||
onSuggestionTermTyped = {
|
||||
actions.onContactSuggestionTermChanged(it, ContactSuggestionsField.BCC)
|
||||
},
|
||||
onSuggestionsDismissed = {
|
||||
actions.onContactSuggestionsDismissed(ContactSuggestionsField.BCC)
|
||||
},
|
||||
onListChanged = {
|
||||
actions.onBccChanged(it.mapNotNull { chipItem -> chipItem.toRecipientUiModel() })
|
||||
}
|
||||
),
|
||||
contactSuggestionState = ContactSuggestionState(
|
||||
areSuggestionsExpanded = areContactSuggestionsExpanded[ContactSuggestionsField.BCC] ?: false,
|
||||
contactSuggestionItems = contactSuggestions[ContactSuggestionsField.BCC]?.map {
|
||||
it.toSuggestionContactItem()
|
||||
} ?: emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = recipientsOpen) {
|
||||
recipientsButtonRotation.animateTo(
|
||||
if (recipientsOpen) RecipientsButtonRotationValues.Open else RecipientsButtonRotationValues.Closed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object RecipientsButtonRotationValues {
|
||||
|
||||
const val Open = 180f
|
||||
const val Closed = 0f
|
||||
}
|
||||
|
||||
private fun ChipItem.toRecipientUiModel(): RecipientUiModel? = when (this) {
|
||||
is ChipItem.Counter -> null
|
||||
is ChipItem.Invalid -> RecipientUiModel.Invalid(value)
|
||||
is ChipItem.Valid -> RecipientUiModel.Valid(value)
|
||||
}
|
||||
|
||||
private fun RecipientUiModel.toChipItem(): ChipItem = when (this) {
|
||||
is RecipientUiModel.Invalid -> ChipItem.Invalid(address)
|
||||
is RecipientUiModel.Valid -> ChipItem.Valid(address)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactSuggestionUiModel.toSuggestionContactItem(): ContactSuggestionItem = when (this) {
|
||||
is ContactSuggestionUiModel.Contact -> ContactSuggestionItem(
|
||||
this.name,
|
||||
this.email,
|
||||
listOf(this.email)
|
||||
)
|
||||
|
||||
is ContactSuggestionUiModel.ContactGroup -> ContactSuggestionItem(
|
||||
this.name,
|
||||
TextUiModel.PluralisedText(
|
||||
value = R.plurals.composer_recipient_suggestion_contacts,
|
||||
count = this.emails.size
|
||||
).string(),
|
||||
this.emails
|
||||
)
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailcomposer.presentation.ui
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import ch.protonmail.android.mailcommon.presentation.compose.FocusableFormScope
|
||||
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
|
||||
import ch.protonmail.android.mailcommon.presentation.model.string
|
||||
import ch.protonmail.android.mailcommon.presentation.ui.MailDivider
|
||||
import ch.protonmail.android.mailcomposer.presentation.R
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ComposerFields
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ContactSuggestionUiModel
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ContactSuggestionsField
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.FocusedFieldType
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.RecipientUiModel
|
||||
import ch.protonmail.android.uicomponents.chips.ChipsListField2
|
||||
import ch.protonmail.android.uicomponents.chips.ContactSuggestionState2
|
||||
import ch.protonmail.android.uicomponents.chips.item.ChipItem
|
||||
import ch.protonmail.android.uicomponents.chips.thenIf
|
||||
import ch.protonmail.android.uicomponents.composer.suggestions.ContactSuggestionItem2
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
|
||||
@Composable
|
||||
internal fun FocusableFormScope<FocusedFieldType>.RecipientFields2(
|
||||
modifier: Modifier = Modifier,
|
||||
fields: ComposerFields,
|
||||
fieldFocusRequesters: Map<FocusedFieldType, FocusRequester>,
|
||||
recipientsOpen: Boolean,
|
||||
emailValidator: (String) -> Boolean,
|
||||
contactSuggestions: Map<ContactSuggestionsField, List<ContactSuggestionUiModel>>,
|
||||
actions: ComposerFormActions,
|
||||
areContactSuggestionsExpanded: Map<ContactSuggestionsField, Boolean>
|
||||
) {
|
||||
val recipientsButtonRotation = remember { Animatable(0F) }
|
||||
val isShowingToSuggestions = areContactSuggestionsExpanded[ContactSuggestionsField.TO] == true
|
||||
val isShowingCcSuggestions = areContactSuggestionsExpanded[ContactSuggestionsField.CC] == true
|
||||
val hasCcBccContent = fields.cc.isNotEmpty() || fields.bcc.isNotEmpty()
|
||||
val shouldShowCcBcc = recipientsOpen || hasCcBccContent
|
||||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
ChipsListField2(
|
||||
label = stringResource(id = R.string.to_prefix),
|
||||
value = fields.to.map { it.toChipItem() },
|
||||
chipValidator = emailValidator,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.testTag(ComposerTestTags.ToRecipient)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.TO),
|
||||
focusRequester = fieldFocusRequesters[FocusedFieldType.TO],
|
||||
actions = ChipsListField2.Actions(
|
||||
onSuggestionTermTyped = {
|
||||
actions.onContactSuggestionTermChanged(it, ContactSuggestionsField.TO)
|
||||
},
|
||||
onSuggestionsDismissed = { actions.onContactSuggestionsDismissed(ContactSuggestionsField.TO) },
|
||||
onListChanged = {
|
||||
actions.onToChanged(it.mapNotNull { chipItem -> chipItem.toRecipientUiModel() })
|
||||
}
|
||||
),
|
||||
contactSuggestionState = ContactSuggestionState2(
|
||||
areSuggestionsExpanded = areContactSuggestionsExpanded[ContactSuggestionsField.TO] ?: false,
|
||||
contactSuggestionItems = contactSuggestions[ContactSuggestionsField.TO]?.map {
|
||||
it.toSuggestionContactItem()
|
||||
} ?: emptyList()
|
||||
),
|
||||
chevronIconContent = {
|
||||
if (!hasCcBccContent) {
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Top)
|
||||
.focusProperties { canFocus = false },
|
||||
onClick = { actions.onToggleRecipients(!recipientsOpen) }
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.thenIf(recipientsButtonRotation.value == RecipientsButtonRotationValues2.Closed) {
|
||||
testTag(ComposerTestTags.ExpandCollapseArrow)
|
||||
}
|
||||
.thenIf(recipientsButtonRotation.value == RecipientsButtonRotationValues2.Open) {
|
||||
testTag(ComposerTestTags.CollapseExpandArrow)
|
||||
}
|
||||
.rotate(recipientsButtonRotation.value)
|
||||
.size(ProtonDimens.SmallIconSize),
|
||||
imageVector = ImageVector.vectorResource(
|
||||
id = me.proton.core.presentation.R.drawable.ic_proton_chevron_down_filled
|
||||
),
|
||||
tint = ProtonTheme.colors.textWeak,
|
||||
contentDescription = stringResource(id = R.string.composer_expand_recipients_button)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldShowCcBcc && !isShowingToSuggestions) {
|
||||
Column {
|
||||
MailDivider()
|
||||
ChipsListField2(
|
||||
label = stringResource(id = R.string.cc_prefix),
|
||||
value = fields.cc.map { it.toChipItem() },
|
||||
chipValidator = emailValidator,
|
||||
modifier = Modifier
|
||||
.testTag(ComposerTestTags.CcRecipient)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.CC),
|
||||
focusRequester = fieldFocusRequesters[FocusedFieldType.CC],
|
||||
actions = ChipsListField2.Actions(
|
||||
onSuggestionTermTyped = {
|
||||
actions.onContactSuggestionTermChanged(it, ContactSuggestionsField.CC)
|
||||
},
|
||||
onSuggestionsDismissed = {
|
||||
actions.onContactSuggestionsDismissed(ContactSuggestionsField.CC)
|
||||
},
|
||||
onListChanged = {
|
||||
actions.onCcChanged(it.mapNotNull { chipItem -> chipItem.toRecipientUiModel() })
|
||||
}
|
||||
),
|
||||
contactSuggestionState = ContactSuggestionState2(
|
||||
areSuggestionsExpanded = areContactSuggestionsExpanded[ContactSuggestionsField.CC] ?: false,
|
||||
contactSuggestionItems = contactSuggestions[ContactSuggestionsField.CC]?.map {
|
||||
it.toSuggestionContactItem()
|
||||
} ?: emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
if (!isShowingCcSuggestions) {
|
||||
MailDivider()
|
||||
ChipsListField2(
|
||||
label = stringResource(id = R.string.bcc_prefix),
|
||||
value = fields.bcc.map { it.toChipItem() },
|
||||
chipValidator = emailValidator,
|
||||
modifier = Modifier
|
||||
.testTag(ComposerTestTags.BccRecipient)
|
||||
.retainFieldFocusOnConfigurationChange(FocusedFieldType.BCC),
|
||||
focusRequester = fieldFocusRequesters[FocusedFieldType.BCC],
|
||||
actions = ChipsListField2.Actions(
|
||||
onSuggestionTermTyped = {
|
||||
actions.onContactSuggestionTermChanged(it, ContactSuggestionsField.BCC)
|
||||
},
|
||||
onSuggestionsDismissed = {
|
||||
actions.onContactSuggestionsDismissed(ContactSuggestionsField.BCC)
|
||||
},
|
||||
onListChanged = {
|
||||
actions.onBccChanged(it.mapNotNull { chipItem -> chipItem.toRecipientUiModel() })
|
||||
}
|
||||
),
|
||||
contactSuggestionState = ContactSuggestionState2(
|
||||
areSuggestionsExpanded = areContactSuggestionsExpanded[ContactSuggestionsField.BCC] ?: false,
|
||||
contactSuggestionItems = contactSuggestions[ContactSuggestionsField.BCC]?.map {
|
||||
it.toSuggestionContactItem()
|
||||
} ?: emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = recipientsOpen) {
|
||||
recipientsButtonRotation.animateTo(
|
||||
if (recipientsOpen) RecipientsButtonRotationValues2.Open else RecipientsButtonRotationValues2.Closed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object RecipientsButtonRotationValues2 {
|
||||
|
||||
const val Open = 180f
|
||||
const val Closed = 0f
|
||||
}
|
||||
|
||||
private fun ChipItem.toRecipientUiModel(): RecipientUiModel? = when (this) {
|
||||
is ChipItem.Counter -> null
|
||||
is ChipItem.Invalid -> RecipientUiModel.Invalid(value)
|
||||
is ChipItem.Valid -> RecipientUiModel.Valid(value)
|
||||
}
|
||||
|
||||
private fun RecipientUiModel.toChipItem(): ChipItem = when (this) {
|
||||
is RecipientUiModel.Invalid -> ChipItem.Invalid(address)
|
||||
is RecipientUiModel.Valid -> ChipItem.Valid(address)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactSuggestionUiModel.toSuggestionContactItem(): ContactSuggestionItem2 = when (this) {
|
||||
is ContactSuggestionUiModel.Contact -> ContactSuggestionItem2.ContactSuggestionItem(
|
||||
this.initial,
|
||||
this.name,
|
||||
this.email,
|
||||
this.email
|
||||
)
|
||||
|
||||
is ContactSuggestionUiModel.ContactGroup -> ContactSuggestionItem2.ContactGroupSuggestionItem(
|
||||
this.name,
|
||||
TextUiModel.PluralisedText(
|
||||
value = R.plurals.composer_recipient_suggestion_contacts,
|
||||
count = this.emails.size
|
||||
).string(),
|
||||
this.emails
|
||||
)
|
||||
}
|
||||
+36
-28
@@ -18,65 +18,73 @@
|
||||
|
||||
package ch.protonmail.android.mailcomposer.presentation.usecase
|
||||
|
||||
import ch.protonmail.android.mailcommon.domain.coroutines.DefaultDispatcher
|
||||
import ch.protonmail.android.mailcommon.presentation.usecase.GetInitials
|
||||
import ch.protonmail.android.mailcomposer.presentation.model.ContactSuggestionUiModel
|
||||
import ch.protonmail.android.mailcontact.domain.model.Contact
|
||||
import ch.protonmail.android.mailcontact.domain.model.ContactGroup
|
||||
import ch.protonmail.android.mailcontact.domain.model.DeviceContact
|
||||
import ch.protonmail.android.mailcontact.domain.model.Contact
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.proton.core.util.kotlin.takeIfNotBlank
|
||||
import javax.inject.Inject
|
||||
|
||||
class SortContactsForSuggestions @Inject constructor() {
|
||||
class SortContactsForSuggestions @Inject constructor(
|
||||
private val getInitials: GetInitials,
|
||||
@DefaultDispatcher private val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
suspend operator fun invoke(
|
||||
contacts: List<Contact>,
|
||||
deviceContacts: List<DeviceContact>,
|
||||
contactGroups: List<ContactGroup>,
|
||||
maxContactAutocompletionCount: Int
|
||||
): List<ContactSuggestionUiModel> {
|
||||
): List<ContactSuggestionUiModel> = withContext(dispatcher) {
|
||||
// Use a temporary map to store unique contacts based on their email address.
|
||||
val temporaryEmailContactMap = mutableMapOf<String, ContactSuggestionUiModel.Contact>()
|
||||
|
||||
val fromContacts = contacts.asSequence().flatMap { contact ->
|
||||
contact.contactEmails.map {
|
||||
contact.copy(
|
||||
contactEmails = listOf(it)
|
||||
) // flatMap into Contacts containing only one ContactEmail because we need to sort by them
|
||||
contact.contactEmails.map { contactEmail ->
|
||||
Triple(contact, contactEmail, Long.MAX_VALUE - contactEmail.lastUsedTime)
|
||||
}
|
||||
}.sortedBy {
|
||||
val lastUsedTimeDescending = Long.MAX_VALUE - it.contactEmails.first().lastUsedTime
|
||||
}.sortedBy { (contact, contactEmail, lastUsedTimeDescending) ->
|
||||
"$lastUsedTimeDescending ${contact.name} ${contactEmail.email}"
|
||||
}.mapNotNull { (contact, contactEmail, _) ->
|
||||
val email = contactEmail.email
|
||||
if (email in temporaryEmailContactMap) return@mapNotNull null
|
||||
|
||||
// LastUsedTime, name, email
|
||||
"$lastUsedTimeDescending ${it.name} ${it.contactEmails.first().email ?: ""}"
|
||||
}.map { contact ->
|
||||
val contactEmail = contact.contactEmails.first()
|
||||
ContactSuggestionUiModel.Contact(
|
||||
name = contactEmail.name.takeIfNotBlank()
|
||||
?: contact.name.takeIfNotBlank()
|
||||
?: contactEmail.email,
|
||||
email = contactEmail.email
|
||||
)
|
||||
?: email,
|
||||
initial = getInitials(contact.name).takeIfNotBlank() ?: "?",
|
||||
email = email
|
||||
).also { temporaryEmailContactMap[email] = it }
|
||||
}
|
||||
|
||||
val fromDeviceContacts = deviceContacts.asSequence().map {
|
||||
val fromDeviceContacts = deviceContacts.asSequence().mapNotNull { deviceContact ->
|
||||
val email = deviceContact.email
|
||||
if (email in temporaryEmailContactMap) return@mapNotNull null
|
||||
|
||||
ContactSuggestionUiModel.Contact(
|
||||
name = it.name,
|
||||
email = it.email
|
||||
)
|
||||
name = deviceContact.name,
|
||||
initial = getInitials(deviceContact.name).takeIfNotBlank() ?: "?",
|
||||
email = email
|
||||
).also { temporaryEmailContactMap[email] = it }
|
||||
}
|
||||
|
||||
val fromContactGroups = contactGroups.asSequence().map { contactGroup ->
|
||||
ContactSuggestionUiModel.ContactGroup(
|
||||
name = contactGroup.name,
|
||||
emails = contactGroup.members.map { it.email }
|
||||
emails = contactGroup.members.map { it.email }.distinct()
|
||||
)
|
||||
}
|
||||
|
||||
val fromDeviceAndContactGroups = (fromDeviceContacts + fromContactGroups).sortedBy {
|
||||
it.name
|
||||
}
|
||||
val fromDeviceAndContactGroups = (fromDeviceContacts + fromContactGroups).sortedBy { it.name }
|
||||
|
||||
return (fromContacts + fromDeviceAndContactGroups)
|
||||
return@withContext (fromContacts + fromDeviceAndContactGroups)
|
||||
.take(maxContactAutocompletionCount)
|
||||
.toList()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-1
@@ -30,6 +30,7 @@ import ch.protonmail.android.mailcommon.domain.model.hasEmailData
|
||||
import ch.protonmail.android.mailcommon.domain.usecase.GetPrimaryAddress
|
||||
import ch.protonmail.android.mailcommon.domain.usecase.ObservePrimaryUserId
|
||||
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
|
||||
import ch.protonmail.android.mailcomposer.domain.annotations.NewContactSuggestionsEnabled
|
||||
import ch.protonmail.android.mailcomposer.domain.model.DecryptedDraftFields
|
||||
import ch.protonmail.android.mailcomposer.domain.model.DraftBody
|
||||
import ch.protonmail.android.mailcomposer.domain.model.DraftFields
|
||||
@@ -160,6 +161,7 @@ class ComposerViewModel @Inject constructor(
|
||||
private val observeMessageExpirationTime: ObserveMessageExpirationTime,
|
||||
private val getExternalRecipients: GetExternalRecipients,
|
||||
private val convertHtmlToPlainText: ConvertHtmlToPlainText,
|
||||
@NewContactSuggestionsEnabled private val isNewContactsSuggestionsEnabled: Boolean,
|
||||
isDeviceContactsSuggestionsEnabled: IsDeviceContactsSuggestionsEnabled,
|
||||
getDecryptedDraftFields: GetDecryptedDraftFields,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
@@ -180,6 +182,9 @@ class ComposerViewModel @Inject constructor(
|
||||
)
|
||||
val state: StateFlow<ComposerDraftState> = mutableState
|
||||
|
||||
// This is a short lived FF, kept outside of the state on purpose.
|
||||
val newContactSuggestionsEnabled = isNewContactsSuggestionsEnabled
|
||||
|
||||
init {
|
||||
val inputDraftId = savedStateHandle.get<String>(ComposerScreen.DraftMessageIdKey)
|
||||
val draftAction = savedStateHandle.get<String>(ComposerScreen.SerializedDraftActionKey)
|
||||
@@ -401,7 +406,6 @@ class ComposerViewModel @Inject constructor(
|
||||
|
||||
is ComposerAction.ContactSuggestionsDismissed -> emitNewStateFor(action)
|
||||
is ComposerAction.DeviceContactsPromptDenied -> onDeviceContactsPromptDenied()
|
||||
is ComposerAction.OnBottomSheetOptionSelected -> emitNewStateFor(action)
|
||||
is ComposerAction.OnAddAttachments -> emitNewStateFor(action)
|
||||
is ComposerAction.OnCloseComposer -> emitNewStateFor(onCloseComposer(action))
|
||||
is ComposerAction.OnSendMessage -> emitNewStateFor(handleOnSendMessage(action))
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Cambiar el remitente</string>
|
||||
<string name="composer_change_sender_paid_feature">Mejore a un plan de pago para cambiar la dirección de remitente</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">No se puede cambiar el remitente porque no pudo obtener la suscripción del usuario. Intente otra vez.</string>
|
||||
<string name="composer_error_store_draft_sender_address">Error al guardar la dirección del remitente de este borrador</string>
|
||||
<string name="composer_error_store_draft_sender_address">Error al guardar la dirección del remitente</string>
|
||||
<string name="composer_error_store_draft_body">Error al guardar el cuerpo de este borrador</string>
|
||||
<string name="composer_error_duplicate_recipient">Destinatario duplicado borrado: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">Error al guardar el asunto de este borrador</string>
|
||||
<string name="composer_error_duplicate_recipient">Destinatarios duplicados borrados</string>
|
||||
<string name="composer_error_store_draft_subject">Error al guardar el asunto</string>
|
||||
<string name="composer_error_store_draft_recipients">Error al guardar los destinatarios de este borrador</string>
|
||||
<string name="composer_error_loading_draft">El contenido de este borrador no está disponible en este momento. Editarlo sobrescribirá cualquier contenido preexistente.</string>
|
||||
<string name="composer_add_attachments_content_description">Añadir archivo adjunto</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Error de envío</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Cerrar</string>
|
||||
<string name="message_sending_error_dialog_text_error">El correo electrónico no puede ser enviado al correo electrónico especificado debido a la siguiente razón:</string>
|
||||
<string name="message_sending_error_dialog_text_error">No se puede enviar el correo electrónico a la dirección indicada debido a la siguiente razón:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">No hay claves de confianza: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">La dirección no existe: %s</string>
|
||||
<string name="set_message_password_title">Cifrar mensaje</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">Longitud de 4 a 21 caracteres</string>
|
||||
<string name="set_message_password_supporting_error_text">La contraseña debe tener entre 4 y 21 caracteres</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Las contraseñas deben coincidir</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Las dos contraseñas no coinciden</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Las contraseñas no coinciden.</string>
|
||||
<string name="set_message_password_button_show">Mostrar contraseña</string>
|
||||
<string name="set_message_password_button_hide">Ocultar contraseña</string>
|
||||
<string name="set_message_password_button_apply">Aplicar contraseña</string>
|
||||
@@ -96,7 +96,7 @@
|
||||
<string name="set_message_password_button_remove_password">Borrar la contraseña</string>
|
||||
<string name="respondInline">Responder entre líneas</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d contacto</item>
|
||||
<item quantity="other">%d contactos</item>
|
||||
<item quantity="one">%d miembro</item>
|
||||
<item quantity="other">%d miembros</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Змяніць адпраўніка</string>
|
||||
<string name="composer_change_sender_paid_feature">Перайдзіце на платны тарыфны план, каб змяняць адрас адпраўніка</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">Немагчыма змяніць адпраўніка, бо не ўдаецца атрымаць падпіску карыстальніка. Паспрабуйце яшчэ раз.</string>
|
||||
<string name="composer_error_store_draft_sender_address">Збой захавання адраса адпраўніка гэтага чарнавіка</string>
|
||||
<string name="composer_error_store_draft_body">Збой захавання цела гэтага чарнавіка</string>
|
||||
<string name="composer_error_duplicate_recipient">Выдалены дублікат атрымальніка: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">Збой захавання тэмы гэтага чарнавіка</string>
|
||||
<string name="composer_error_store_draft_sender_address">Sender address wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_body">Draft email body wasn\'t saved</string>
|
||||
<string name="composer_error_duplicate_recipient">Дубліраваныя атрымальнік(і) выдалены</string>
|
||||
<string name="composer_error_store_draft_subject">Subject line wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_recipients">Збой захавання атрымальнікаў гэтага чарнавіка</string>
|
||||
<string name="composer_error_loading_draft">Змесціва гэтага чарнавіка недаступна ў цяперашні час. Рэдагаванне перавызначыць любое змесціва, якое было створана да гэтага.</string>
|
||||
<string name="composer_add_attachments_content_description">Дадаць далучэнні</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Памылка адпраўкі</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Закрыць</string>
|
||||
<string name="message_sending_error_dialog_text_error">Ваш ліст немагчыма адправіць на ўведзены адрас электроннай пошты па наступнай прычыне:</string>
|
||||
<string name="message_sending_error_dialog_text_error">Your email cannot be sent to the address entered due to the following reason:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">Адсутнічаюць давераныя ключы: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">Адрас не існуе: %s</string>
|
||||
<string name="set_message_password_title">Зашыфраваць паведамленне</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">Ад 4 да 21 сімвала</string>
|
||||
<string name="set_message_password_supporting_error_text">Ваш пароль павінен мець даўжыню ад 4 да 21 сімвала</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Паролі павінны супадаць</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Паролі не супадаюць</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Passwords do not match</string>
|
||||
<string name="set_message_password_button_show">Паказаць пароль</string>
|
||||
<string name="set_message_password_button_hide">Схаваць пароль</string>
|
||||
<string name="set_message_password_button_apply">Ужыць пароль</string>
|
||||
@@ -96,9 +96,9 @@
|
||||
<string name="set_message_password_button_remove_password">Выдаліць пароль</string>
|
||||
<string name="respondInline">Адказаць з цытаваннем</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d кантакт</item>
|
||||
<item quantity="few">%d кантакты</item>
|
||||
<item quantity="many">%d кантактаў</item>
|
||||
<item quantity="other">%d кантактаў</item>
|
||||
<item quantity="one">%d удзельнік</item>
|
||||
<item quantity="few">%d удзельнікі</item>
|
||||
<item quantity="many">%d удзельнікаў</item>
|
||||
<item quantity="other">%d удзельнікаў</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Canvia el remitent</string>
|
||||
<string name="composer_change_sender_paid_feature">Milloreu a un pla de pagament per canviar l\'adreça del remitent.</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">No es pot canviar el remitent perquè no s\'ha pogut obtenir la subscripció de l\'usuari. Torna-ho a provar.</string>
|
||||
<string name="composer_error_store_draft_sender_address">S\'ha produït un error en desar l\'adreça del remitent..</string>
|
||||
<string name="composer_error_store_draft_sender_address">S\'ha produït un error en desar l\'adreça del remitent.</string>
|
||||
<string name="composer_error_store_draft_body">S\'ha produït un error en desar el cos de l\'esborrany.</string>
|
||||
<string name="composer_error_duplicate_recipient">Destinatari duplicat esborrat: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">S\'ha produït un error en desar l\'assumpte de l\'esborrany.</string>
|
||||
<string name="composer_error_duplicate_recipient">Duplicated recipient(s) removed</string>
|
||||
<string name="composer_error_store_draft_subject">S\'ha produït un error en desar l\'assumpte.</string>
|
||||
<string name="composer_error_store_draft_recipients">S\'ha produït un error en desar els destinataris de l\'esborrany.</string>
|
||||
<string name="composer_error_loading_draft">El contingut d\'aquest esborrany no està disponible en aquest moment. L\'edició substituirà qualsevol contingut preexistent.</string>
|
||||
<string name="composer_add_attachments_content_description">Afegiu fitxers adjunts</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Error d\'enviament</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Tanca</string>
|
||||
<string name="message_sending_error_dialog_text_error">El vostre correu electrònic no es pot enviar al correu electrònic introduït pel motiu següent:</string>
|
||||
<string name="message_sending_error_dialog_text_error">El vostre correu electrònic no es pot enviar a l\'adreça introduïda pel motiu següent:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">No hi ha claus de confiança: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">L\'adreça no existeix: %s</string>
|
||||
<string name="set_message_password_title">Xifra el missatge</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">Longitud de 4 a 21 caràcters</string>
|
||||
<string name="set_message_password_supporting_error_text">La contrasenya ha de tenir entre 4 i 21 caràcters</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Les contrasenyes han de coincidir</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Les 2 contrasenyes no coincideixen</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Les contrasenyes no coincideixen.</string>
|
||||
<string name="set_message_password_button_show">Mostra la contrasenya</string>
|
||||
<string name="set_message_password_button_hide">Amaga la contrasenya</string>
|
||||
<string name="set_message_password_button_apply">Aplica la contrasenya</string>
|
||||
@@ -96,7 +96,7 @@
|
||||
<string name="set_message_password_button_remove_password">Esborra la contrasenya</string>
|
||||
<string name="respondInline">Respon en línia</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d contacte</item>
|
||||
<item quantity="other">%d contactes</item>
|
||||
<item quantity="one">%d membre</item>
|
||||
<item quantity="other">%d membres</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Změnit odesílatele</string>
|
||||
<string name="composer_change_sender_paid_feature">Přejděte na placený tarif pro změnu adresy odesílatele</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">Nelze změnit odesílatele, protože selhalo získání uživatelského předplatného. Zkuste to znovu.</string>
|
||||
<string name="composer_error_store_draft_sender_address">Uložení adresy odesílatele tohoto návrhu se nezdařilo</string>
|
||||
<string name="composer_error_store_draft_body">Uložení těla tohoto návrhu se nezdařilo</string>
|
||||
<string name="composer_error_duplicate_recipient">Odstraněn duplicitní příjemce: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">Uložení předmětu tohoto návrhu se nezdařilo</string>
|
||||
<string name="composer_error_store_draft_sender_address">Sender address wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_body">Draft email body wasn\'t saved</string>
|
||||
<string name="composer_error_duplicate_recipient">Duplicitní příjemci odebráni</string>
|
||||
<string name="composer_error_store_draft_subject">Subject line wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_recipients">Uložení příjemců tohoto konceptu selhalo</string>
|
||||
<string name="composer_error_loading_draft">Obsah tohoto návrhu není v tuto chvíli k dispozici. Úpravy přepíší veškerý již existující obsah.</string>
|
||||
<string name="composer_add_attachments_content_description">Přidat přílohy</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Chyba při odesílání</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Zavřít</string>
|
||||
<string name="message_sending_error_dialog_text_error">Váš e-mail nelze odeslat na zadanou e-mailovou adresu z následujícího důvodu:</string>
|
||||
<string name="message_sending_error_dialog_text_error">Your email cannot be sent to the address entered due to the following reason:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">Neexistují žádné důvěryhodné klíče: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">Adresa neexistuje: %s</string>
|
||||
<string name="set_message_password_title">Šifrovat zprávu</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">4 až 21 znaků</string>
|
||||
<string name="set_message_password_supporting_error_text">Heslo musí mít délku 4 až 21 znaků</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Hesla se musí shodovat</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Tyto 2 hesla nesouhlasí</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Passwords do not match</string>
|
||||
<string name="set_message_password_button_show">Zobrazit heslo</string>
|
||||
<string name="set_message_password_button_hide">Skrýt heslo</string>
|
||||
<string name="set_message_password_button_apply">Použít heslo</string>
|
||||
@@ -96,9 +96,9 @@
|
||||
<string name="set_message_password_button_remove_password">Odstranit heslo</string>
|
||||
<string name="respondInline">Odpovědět v řádku</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d kontakt</item>
|
||||
<item quantity="few">%d kontakty</item>
|
||||
<item quantity="many">%d kontaktů</item>
|
||||
<item quantity="other">%d kontaktů</item>
|
||||
<item quantity="one">%d člen</item>
|
||||
<item quantity="few">%d členové</item>
|
||||
<item quantity="many">%d členů</item>
|
||||
<item quantity="other">%d členů</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Skift afsender</string>
|
||||
<string name="composer_change_sender_paid_feature">Opgradér til et betalt abonnement for at ændre afsenderadresse</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">Kan ikke ændre afsender, da hentning af brugerens abonnement mislykkedes. Prøv igen.</string>
|
||||
<string name="composer_error_store_draft_sender_address">Lagring af denne kladdes afsenderadresse mislykkedes</string>
|
||||
<string name="composer_error_store_draft_body">Opbevaring af denne kladdes indholdstekst mislykkedes</string>
|
||||
<string name="composer_error_duplicate_recipient">Fjernet dobbelt modtager: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">Lagring af denne kladdes emne mislykkedes</string>
|
||||
<string name="composer_error_store_draft_sender_address">Sender address wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_body">Draft email body wasn\'t saved</string>
|
||||
<string name="composer_error_duplicate_recipient">Dublet-modtager(e) fjernet</string>
|
||||
<string name="composer_error_store_draft_subject">Subject line wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_recipients">Lagring af denne kladdes modtagere mislykkedes</string>
|
||||
<string name="composer_error_loading_draft">Indholdet af denne kladde er ikke tilgængeligt på nuværende tidspunkt. Redigering vil overskrive alt allerede eksisterende indhold.</string>
|
||||
<string name="composer_add_attachments_content_description">Tilføj vedhæftninger</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Afsendelsesfejl</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Luk</string>
|
||||
<string name="message_sending_error_dialog_text_error">Din e-mail kan ikke sendes til den indtastede e-mail af følgende grund:</string>
|
||||
<string name="message_sending_error_dialog_text_error">Your email cannot be sent to the address entered due to the following reason:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">Der er ingen betroede nøgler: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">Adresse findes ikke: %s</string>
|
||||
<string name="set_message_password_title">Krypter besked</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">4 til 21 tegn lang</string>
|
||||
<string name="set_message_password_supporting_error_text">Adgangskoden skal være mellem 4 og 21 tegn lang</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Adgangskoder skal være ens</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">De to adgangskoder stemmer ikke overens</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Passwords do not match</string>
|
||||
<string name="set_message_password_button_show">Vis adgangskode</string>
|
||||
<string name="set_message_password_button_hide">Skjul adgangskode</string>
|
||||
<string name="set_message_password_button_apply">Anvend adgangskode</string>
|
||||
@@ -96,7 +96,7 @@
|
||||
<string name="set_message_password_button_remove_password">Fjern adgangskode</string>
|
||||
<string name="respondInline">Besvar in-line</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d kontakt</item>
|
||||
<item quantity="other">%d kontakter</item>
|
||||
<item quantity="one">%d medlem</item>
|
||||
<item quantity="other">%d medlemmer</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">Fehler beim Ändern des Absenders, da das Abonnement des Benutzers nicht abgerufen werden konnte. Bitte versuche es erneut.</string>
|
||||
<string name="composer_error_store_draft_sender_address">Das Speichern der Absenderadresse dieses Entwurfs ist fehlgeschlagen.</string>
|
||||
<string name="composer_error_store_draft_body">Das Speichern des Inhalts dieses Entwurfs ist fehlgeschlagen.</string>
|
||||
<string name="composer_error_duplicate_recipient">Doppelten Empfänger entfernt: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">Das Speichern des Betreffs dieses Entwurfs ist fehlgeschlagen.</string>
|
||||
<string name="composer_error_duplicate_recipient">Doppelte Empfänger entfernt</string>
|
||||
<string name="composer_error_store_draft_subject">Subject line wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_recipients">Das Speichern der Empfänger dieses Entwurfs ist fehlgeschlagen</string>
|
||||
<string name="composer_error_loading_draft">Der Inhalt dieses Entwurfs ist zu diesem Zeitpunkt nicht verfügbar. Durch die Bearbeitung wird der bereits vorhandene Inhalt überschrieben.</string>
|
||||
<string name="composer_add_attachments_content_description">Anhänge hinzufügen</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Sendefehler</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Schließen</string>
|
||||
<string name="message_sending_error_dialog_text_error">Deine E-Mail kann aus folgendem Grund nicht an die angegebene E-Mail-Adresse gesendet werden:</string>
|
||||
<string name="message_sending_error_dialog_text_error">Your email cannot be sent to the address entered due to the following reason:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">Es gibt keine vertrauenswürdigen Schlüssel: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">Adresse existiert nicht: %s</string>
|
||||
<string name="set_message_password_title">Nachricht verschlüsseln</string>
|
||||
@@ -96,7 +96,7 @@
|
||||
<string name="set_message_password_button_remove_password">Passwort entfernen</string>
|
||||
<string name="respondInline">Eingebettet antworten</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d Kontakt</item>
|
||||
<item quantity="other">%d Kontakte</item>
|
||||
<item quantity="one">%d Mitglied</item>
|
||||
<item quantity="other">%d Mitglieder</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Αλλαγή αποστολέα</string>
|
||||
<string name="composer_change_sender_paid_feature">Αναβαθμίστε σε πρόγραμμα επί πληρωμή ώστε να αλλάξετε τη διεύθυνση του αποστολέα</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">Δεν είναι δυνατή η αλλαγή αποστολέα καθώς απέτυχε η λήψη της συνδρομής του χρήστη. Δοκιμάστε ξανά.</string>
|
||||
<string name="composer_error_store_draft_sender_address">Αποτυχία αποθήκευσης της διεύθυνσης αποστολέα αυτού του πρόχειρου</string>
|
||||
<string name="composer_error_store_draft_body">Αποτυχία αποθήκευσης του σώματος αυτού του πρόχειρου</string>
|
||||
<string name="composer_error_duplicate_recipient">Αφαιρέθηκε διπλότυπος παραλήπτης: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">Η αποθήκευση αυτού του θέματος του πρόχειρου απέτυχε</string>
|
||||
<string name="composer_error_store_draft_sender_address">Sender address wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_body">Draft email body wasn\'t saved</string>
|
||||
<string name="composer_error_duplicate_recipient">Οι διπλότυποι παραλήπτες καταργήθηκαν</string>
|
||||
<string name="composer_error_store_draft_subject">Subject line wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_recipients">Η αποθήκευση των παραληπτών αυτού του πρόχειρου απέτυχε</string>
|
||||
<string name="composer_error_loading_draft">Το περιεχόμενο αυτού του πρόχειρου δεν είναι διαθέσιμο προς το παρόν. Η επεξεργασία θα αντικαταστήσει τυχόν προϋπάρχον περιεχόμενο.</string>
|
||||
<string name="composer_add_attachments_content_description">Προσθήκη συνημμένων</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Σφάλμα αποστολής</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Κλείσιμο</string>
|
||||
<string name="message_sending_error_dialog_text_error">Το μήνυμα σας δεν μπορεί να σταλεί στη διεύθυνση ηλεκτρονικού ταχυδρομείου που καταχωρήσατε για τον ακόλουθο λόγο:</string>
|
||||
<string name="message_sending_error_dialog_text_error">Your email cannot be sent to the address entered due to the following reason:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">Δεν υπάρχουν Αξιόπιστα Κλειδιά: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">Η διεύθυνση δεν υπάρχει: %s</string>
|
||||
<string name="set_message_password_title">Κρυπτογράφηση μηνύματος</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">μήκος 4 έως 21 χαρακτήρες</string>
|
||||
<string name="set_message_password_supporting_error_text">Ο κωδικός πρόσβασης πρέπει να αποτελείται από 4 έως 21 χαρακτήρες</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Οι κωδικοί πρόσβασης πρέπει να ταυτίζονται</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Οι δυο κωδικοί πρόσβασης δεν είναι ταυτόσημοι</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Passwords do not match</string>
|
||||
<string name="set_message_password_button_show">Εμφάνιση κωδικού πρόσβασης</string>
|
||||
<string name="set_message_password_button_hide">Απόκρυψη κωδικού πρόσβασης</string>
|
||||
<string name="set_message_password_button_apply">Εφαρμογή κωδικού πρόσβασης</string>
|
||||
@@ -96,7 +96,7 @@
|
||||
<string name="set_message_password_button_remove_password">Αφαίρεση κωδικού πρόσβασης</string>
|
||||
<string name="respondInline">Απάντηση μέσα στην γραμμή</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d επαφή</item>
|
||||
<item quantity="other">%d επαφές</item>
|
||||
<item quantity="one">%d μέλος</item>
|
||||
<item quantity="other">%d μέλη</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Cambiar el remitente</string>
|
||||
<string name="composer_change_sender_paid_feature">Mejora a un plan de pago para cambiar la dirección de remitente</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">No se puede cambiar el remitente porque no se ha podido obtener la suscripción del usuario. Intenta otra vez.</string>
|
||||
<string name="composer_error_store_draft_sender_address">Error al guardar la dirección del remitente de este borrador</string>
|
||||
<string name="composer_error_store_draft_sender_address">Error al guardar la dirección del remitente</string>
|
||||
<string name="composer_error_store_draft_body">Error al guardar el cuerpo de este borrador</string>
|
||||
<string name="composer_error_duplicate_recipient">Destinatario duplicado borrado: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">Error al guardar el asunto de este borrador</string>
|
||||
<string name="composer_error_duplicate_recipient">Se han borrado un o más destinatarios duplicados.</string>
|
||||
<string name="composer_error_store_draft_subject">Error al guardar el asunto</string>
|
||||
<string name="composer_error_store_draft_recipients">Error al guardar los destinatarios de este borrador</string>
|
||||
<string name="composer_error_loading_draft">El contenido de este borrador no está disponible en este momento. Editarlo sobrescribirá cualquier contenido preexistente.</string>
|
||||
<string name="composer_add_attachments_content_description">Añadir archivos adjuntos</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Error de envío</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Cerrar</string>
|
||||
<string name="message_sending_error_dialog_text_error">Tu correo electrónico no puede ser enviado al correo electrónico especificado debido a la siguiente razón:</string>
|
||||
<string name="message_sending_error_dialog_text_error">No se puede enviar el correo electrónico a la dirección indicada debido a la siguiente razón:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">No hay claves de confianza: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">La dirección no existe: %s</string>
|
||||
<string name="set_message_password_title">Cifrar el mensaje</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">4 a 21 caracteres</string>
|
||||
<string name="set_message_password_supporting_error_text">La contraseña debe tener entre 4 y 21 caracteres.</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Las contraseñas deben coincidir.</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Las dos contraseñas no coinciden</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Las contraseñas no coinciden.</string>
|
||||
<string name="set_message_password_button_show">Mostrar la contraseña</string>
|
||||
<string name="set_message_password_button_hide">Ocultar la contraseña</string>
|
||||
<string name="set_message_password_button_apply">Aplicar la contraseña</string>
|
||||
@@ -96,7 +96,7 @@
|
||||
<string name="set_message_password_button_remove_password">Borrar la contraseña</string>
|
||||
<string name="respondInline">Responder en línea</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d contacto</item>
|
||||
<item quantity="other">%d contactos</item>
|
||||
<item quantity="one">%d miembro</item>
|
||||
<item quantity="other">%d miembros</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Modifier l\'expéditeur</string>
|
||||
<string name="composer_change_sender_paid_feature">Passez à un abonnement payant pour modifier l\'adresse de l\'expéditeur.</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">Impossible de changer l\'expéditeur : l\'abonnement de l\'utilisateur n\'a pu être obtenu. Veuillez réessayer.</string>
|
||||
<string name="composer_error_store_draft_sender_address">L\'enregistrement de l\'adresse de l\'expéditeur du brouillon n\'a pas abouti.</string>
|
||||
<string name="composer_error_store_draft_sender_address">L\'enregistrement de l\'adresse de l\'expéditeur n\'a pas abouti.</string>
|
||||
<string name="composer_error_store_draft_body">L\'enregistrement du corps du brouillon n\'a pas abouti.</string>
|
||||
<string name="composer_error_duplicate_recipient">Destinataire dupliqué retiré : %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">L\'enregistrement de l\'objet du brouillon n\'a pas abouti.</string>
|
||||
<string name="composer_error_duplicate_recipient">Un ou plusieurs destinataires dupliqués ont été retirés.</string>
|
||||
<string name="composer_error_store_draft_subject">L\'enregistrement de l\'objet n\'a pas abouti.</string>
|
||||
<string name="composer_error_store_draft_recipients">L\'enregistrement des destinataires du brouillon n\'a pas abouti.</string>
|
||||
<string name="composer_error_loading_draft">Le contenu de ce brouillon n\'est pas disponible pour l\'instant. Sa modification remplacera tout contenu déjà existant.</string>
|
||||
<string name="composer_add_attachments_content_description">Ajouter des pièces jointes</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">4 à 21 caractères</string>
|
||||
<string name="set_message_password_supporting_error_text">Le mot de passe doit contenir entre 4 et 21 caractères.</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Les mots de passe doivent être identiques.</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Les 2 mots de passe ne sont pas identiques.</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Les mots de passe ne correspondent pas.</string>
|
||||
<string name="set_message_password_button_show">Afficher le mot de passe</string>
|
||||
<string name="set_message_password_button_hide">Masquer le mot de passe</string>
|
||||
<string name="set_message_password_button_apply">Appliquer le mot de passe</string>
|
||||
@@ -96,7 +96,7 @@
|
||||
<string name="set_message_password_button_remove_password">Retirer le mot de passe</string>
|
||||
<string name="respondInline">Répondre dans le message</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d contact</item>
|
||||
<item quantity="other">%d contacts</item>
|
||||
<item quantity="one">%d membre</item>
|
||||
<item quantity="other">%d membres</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Change sender</string>
|
||||
<string name="composer_change_sender_paid_feature">Upgrade to a paid plan to change the sender address</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">Cannot change sender as getting user\'s subscription failed. Try again.</string>
|
||||
<string name="composer_error_store_draft_sender_address">Storing this draft\'s sender address failed</string>
|
||||
<string name="composer_error_store_draft_body">Storing this draft\'s body failed</string>
|
||||
<string name="composer_error_duplicate_recipient">Removed duplicate recipient: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">Storing this draft\'s subject failed</string>
|
||||
<string name="composer_error_store_draft_sender_address">Sender address wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_body">Draft email body wasn\'t saved</string>
|
||||
<string name="composer_error_duplicate_recipient">Duplicated recipient(s) removed</string>
|
||||
<string name="composer_error_store_draft_subject">Subject line wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_recipients">Storing this draft\'s recipients failed</string>
|
||||
<string name="composer_error_loading_draft">The content of this draft is not available at this time. Editing will override any pre-existing content.</string>
|
||||
<string name="composer_add_attachments_content_description">Dodaj privitke</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Sending error</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Zatvori</string>
|
||||
<string name="message_sending_error_dialog_text_error">Your email cannot be sent to the email entered due to the following reason:</string>
|
||||
<string name="message_sending_error_dialog_text_error">Your email cannot be sent to the address entered due to the following reason:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">There are no Trusted Keys: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">Address does not exist: %s</string>
|
||||
<string name="set_message_password_title">Encrypt message</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">4 do 21 znakova dugo</string>
|
||||
<string name="set_message_password_supporting_error_text">The password must be between 4 and 21 characters</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Lozinke se moraju poklapati</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Dvije lozinke se ne podudaraju</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Passwords do not match</string>
|
||||
<string name="set_message_password_button_show">Pokaži lozinku</string>
|
||||
<string name="set_message_password_button_hide">Sakrij lozinku</string>
|
||||
<string name="set_message_password_button_apply">Primjeni lozinku</string>
|
||||
@@ -96,8 +96,8 @@
|
||||
<string name="set_message_password_button_remove_password">Ukloni lozinku</string>
|
||||
<string name="respondInline">Odgovori u ravnini s ostalim tekstom</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d kontakt</item>
|
||||
<item quantity="few">%d contacts</item>
|
||||
<item quantity="other">%d contacts</item>
|
||||
<item quantity="one">%d member</item>
|
||||
<item quantity="few">%d members</item>
|
||||
<item quantity="other">%d members</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Feladó módosítása</string>
|
||||
<string name="composer_change_sender_paid_feature">Váltson előfizetéses csomagra, a feladó címének megváltoztatásához</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">A feladó nem változtatható meg, mert az előfizetés betöltése nem sikerült. Próbálja újra.</string>
|
||||
<string name="composer_error_store_draft_sender_address">A vázlat feladójának tárolása nem sikerült</string>
|
||||
<string name="composer_error_store_draft_body">A vázlat szövegének tárolása nem sikerült</string>
|
||||
<string name="composer_error_duplicate_recipient">Kettőzött címzett eltávolítva: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">A vázlat tárgyának tárolása nem sikerült</string>
|
||||
<string name="composer_error_store_draft_sender_address">Sender address wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_body">Draft email body wasn\'t saved</string>
|
||||
<string name="composer_error_duplicate_recipient">Duplicated recipient(s) removed</string>
|
||||
<string name="composer_error_store_draft_subject">Subject line wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_recipients">A vázlat címzetteinek tárolása nem sikerült</string>
|
||||
<string name="composer_error_loading_draft">Ennek a piszkozatnak a tartalma jelenleg nem elérhető. Szerkesztése felülírja az esetlegesen már korábban hozzáadott tartalmat.</string>
|
||||
<string name="composer_add_attachments_content_description">Melléklet hozzáadása</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Hiba a küldés közben</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Bezárás</string>
|
||||
<string name="message_sending_error_dialog_text_error">A levél nem küldhető el a megadott e-mail címre a következő ok miatt:</string>
|
||||
<string name="message_sending_error_dialog_text_error">Your email cannot be sent to the address entered due to the following reason:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">Nincs megbízható kulcs: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">Cím nem létezik: %s</string>
|
||||
<string name="set_message_password_title">Üzenet titkosítása</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">4-től 21 karakter hosszúságig</string>
|
||||
<string name="set_message_password_supporting_error_text">A jelszónak legalább 4, és legfeljebb 21 karakter hosszúnak kell lennie</string>
|
||||
<string name="set_message_password_supporting_text_repeat">A jelszavaknak egyezniük kell</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">A két jelszó nem egyezik</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Passwords do not match</string>
|
||||
<string name="set_message_password_button_show">Jelszó megjelenítése</string>
|
||||
<string name="set_message_password_button_hide">Jelszó elrejtése</string>
|
||||
<string name="set_message_password_button_apply">Jelszó alkalmazása</string>
|
||||
@@ -96,7 +96,7 @@
|
||||
<string name="set_message_password_button_remove_password">Jelszó eltávolítása</string>
|
||||
<string name="respondInline">Válaszolás az eredeti levélben</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="one">%d contact</item>
|
||||
<item quantity="other">%d contacts</item>
|
||||
<item quantity="one">%d member</item>
|
||||
<item quantity="other">%d members</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<string name="change_sender_button_content_description">Ubah pengirim</string>
|
||||
<string name="composer_change_sender_paid_feature">Tingkatkan ke paket berbayar untuk mengubah alamat pengirim</string>
|
||||
<string name="composer_error_change_sender_failed_getting_subscription">Pengirim tidak dapat diubah karena status langganan pengguna gagal dimuat. Silakan coba lagi.</string>
|
||||
<string name="composer_error_store_draft_sender_address">Alamat pengirim draf gagal disimpan</string>
|
||||
<string name="composer_error_store_draft_body">Isi draf gagal disimpan</string>
|
||||
<string name="composer_error_duplicate_recipient">Penerima duplikat dihapus: %1$s</string>
|
||||
<string name="composer_error_store_draft_subject">Subjek draf gagal disimpan</string>
|
||||
<string name="composer_error_store_draft_sender_address">Sender address wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_body">Draft email body wasn\'t saved</string>
|
||||
<string name="composer_error_duplicate_recipient">Penerima duplikat dihapus</string>
|
||||
<string name="composer_error_store_draft_subject">Subject line wasn\'t saved</string>
|
||||
<string name="composer_error_store_draft_recipients">Penerima draf gagal disimpan</string>
|
||||
<string name="composer_error_loading_draft">Konten draf ini sedang tidak tersedia. Dengan menyunting pesan, isi draf yang telah ada sebelumnya akan tertimpa.</string>
|
||||
<string name="composer_add_attachments_content_description">Tambah lampiran</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- Message sending error dialog -->
|
||||
<string name="message_sending_error_dialog_header">Pesan gagal dikirim</string>
|
||||
<string name="message_sending_error_dialog_button_dismiss">Tutup</string>
|
||||
<string name="message_sending_error_dialog_text_error">Email Anda tidak dapat dikirim ke alamat email yang dituju karena alasan berikut:</string>
|
||||
<string name="message_sending_error_dialog_text_error">Your email cannot be sent to the address entered due to the following reason:</string>
|
||||
<string name="message_sending_error_dialog_text_reason_no_trusted_keys">Kunci Terpercaya tidak ada: %s</string>
|
||||
<string name="message_sending_error_dialog_text_reason_address_disabled">Alamat tidak ada: %s</string>
|
||||
<string name="set_message_password_title">Enkripsi pesan</string>
|
||||
@@ -88,7 +88,7 @@
|
||||
<string name="set_message_password_supporting_text">antara 4 hingga 21 karakter</string>
|
||||
<string name="set_message_password_supporting_error_text">Kata sandi harus mengandung antara 4 dan 21 karakter</string>
|
||||
<string name="set_message_password_supporting_text_repeat">Kata sandi harus sesuai</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Kedua kata sandi tidak sesuai</string>
|
||||
<string name="set_message_password_supporting_error_text_repeat">Passwords do not match</string>
|
||||
<string name="set_message_password_button_show">Tampilkan kata sandi</string>
|
||||
<string name="set_message_password_button_hide">Sembunyikan kata sandi</string>
|
||||
<string name="set_message_password_button_apply">Terapkan kata sandi</string>
|
||||
@@ -96,6 +96,6 @@
|
||||
<string name="set_message_password_button_remove_password">Hapus kata sandi</string>
|
||||
<string name="respondInline">Tanggapi secara in-line</string>
|
||||
<plurals name="composer_recipient_suggestion_contacts">
|
||||
<item quantity="other">%d kontak</item>
|
||||
<item quantity="other">%d anggota</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user