diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/images/icons/PersonsCircleAdd.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/images/icons/PersonsCircleAdd.kt new file mode 100644 index 000000000..76d01438e --- /dev/null +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/images/icons/PersonsCircleAdd.kt @@ -0,0 +1,132 @@ +package com.infomaniak.swisstransfer.ui.images.icons + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.infomaniak.swisstransfer.ui.images.AppImages +import com.infomaniak.swisstransfer.ui.images.AppImages.AppIcons +import androidx.compose.ui.graphics.StrokeCap.Companion.Round as strokeCapRound +import androidx.compose.ui.graphics.StrokeJoin.Companion.Round as strokeJoinRound + +val AppIcons.PersonsCircleAdd: ImageVector + get() { + if (_personsCircleAdd != null) { + return _personsCircleAdd!! + } + _personsCircleAdd = Builder( + name = "PersonsCircleAdd", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + group { + path( + fill = SolidColor(Color(0xFF9F9F9F)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(18.0f, 12.0f) + curveTo(19.591f, 12.0f, 21.117f, 12.633f, 22.242f, 13.758f) + curveTo(23.367f, 14.883f, 24.0f, 16.409f, 24.0f, 18.0f) + curveTo(24.0f, 19.591f, 23.367f, 21.117f, 22.242f, 22.242f) + curveTo(21.117f, 23.367f, 19.591f, 24.0f, 18.0f, 24.0f) + curveTo(16.409f, 24.0f, 14.883f, 23.367f, 13.758f, 22.242f) + curveTo(12.633f, 21.117f, 12.0f, 19.591f, 12.0f, 18.0f) + curveTo(12.0f, 16.409f, 12.633f, 14.883f, 13.758f, 13.758f) + curveTo(14.883f, 12.633f, 16.409f, 12.0f, 18.0f, 12.0f) + close() + moveTo(18.0f, 14.0f) + curveTo(17.586f, 14.0f, 17.25f, 14.336f, 17.25f, 14.75f) + verticalLineTo(17.25f) + horizontalLineTo(14.75f) + curveTo(14.336f, 17.25f, 14.0f, 17.586f, 14.0f, 18.0f) + curveTo(14.0f, 18.414f, 14.336f, 18.75f, 14.75f, 18.75f) + horizontalLineTo(17.25f) + verticalLineTo(21.25f) + curveTo(17.25f, 21.664f, 17.586f, 22.0f, 18.0f, 22.0f) + curveTo(18.414f, 22.0f, 18.75f, 21.664f, 18.75f, 21.25f) + verticalLineTo(18.75f) + horizontalLineTo(21.25f) + curveTo(21.664f, 18.75f, 22.0f, 18.414f, 22.0f, 18.0f) + curveTo(22.0f, 17.586f, 21.664f, 17.25f, 21.25f, 17.25f) + horizontalLineTo(18.75f) + verticalLineTo(14.75f) + curveTo(18.75f, 14.336f, 18.414f, 14.0f, 18.0f, 14.0f) + close() + } + path( + fill = SolidColor(Color(0x00000000)), + stroke = SolidColor(Color(0xFF9F9F9F)), + strokeLineWidth = 1.5f, + strokeLineCap = strokeCapRound, + strokeLineJoin = strokeJoinRound, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(13.071f, 10.047f) + curveTo(13.58f, 9.824f, 14.134f, 9.705f, 14.7f, 9.705f) + curveTo(15.601f, 9.705f, 16.47f, 10.004f, 17.175f, 10.548f) + moveTo(2.85f, 4.046f) + curveTo(2.85f, 4.92f, 3.197f, 5.758f, 3.816f, 6.377f) + curveTo(4.435f, 6.995f, 5.274f, 7.342f, 6.15f, 7.342f) + curveTo(7.025f, 7.342f, 7.864f, 6.995f, 8.483f, 6.377f) + curveTo(9.102f, 5.758f, 9.449f, 4.92f, 9.449f, 4.046f) + curveTo(9.449f, 3.172f, 9.102f, 2.333f, 8.483f, 1.715f) + curveTo(7.864f, 1.097f, 7.025f, 0.75f, 6.15f, 0.75f) + curveTo(5.274f, 0.75f, 4.435f, 1.097f, 3.816f, 1.715f) + curveTo(3.197f, 2.333f, 2.85f, 3.172f, 2.85f, 4.046f) + close() + moveTo(12.225f, 6.216f) + curveTo(12.225f, 6.872f, 12.486f, 7.501f, 12.95f, 7.964f) + curveTo(13.414f, 8.428f, 14.044f, 8.688f, 14.7f, 8.688f) + curveTo(15.356f, 8.688f, 15.986f, 8.428f, 16.45f, 7.964f) + curveTo(16.914f, 7.501f, 17.175f, 6.872f, 17.175f, 6.216f) + curveTo(17.175f, 5.561f, 16.914f, 4.932f, 16.45f, 4.468f) + curveTo(15.986f, 4.005f, 15.356f, 3.744f, 14.7f, 3.744f) + curveTo(14.044f, 3.744f, 13.414f, 4.005f, 12.95f, 4.468f) + curveTo(12.486f, 4.932f, 12.225f, 5.561f, 12.225f, 6.216f) + close() + moveTo(0.75f, 13.75f) + curveTo(0.75f, 12.32f, 1.319f, 10.948f, 2.332f, 9.936f) + curveTo(3.344f, 8.925f, 4.718f, 8.357f, 6.15f, 8.357f) + curveTo(7.582f, 8.357f, 8.955f, 8.925f, 9.968f, 9.936f) + curveTo(10.98f, 10.948f, 11.549f, 12.32f, 11.549f, 13.75f) + horizontalLineTo(0.75f) + close() + } + } + }.build() + return _personsCircleAdd!! + } + +private var _personsCircleAdd: ImageVector? = null + +@Preview +@Composable +private fun Preview() { + Box { + Image( + imageVector = AppIcons.PersonsCircleAdd, + contentDescription = null, + modifier = Modifier.size(AppImages.previewSize) + ) + } +} diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/PickFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/PickFilesScreen.kt index 92f767639..812b41135 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/PickFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/PickFilesScreen.kt @@ -18,10 +18,18 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer.pickfiles import android.Manifest +import android.app.Activity import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_PICK +import android.net.Uri import android.os.Build.VERSION.SDK_INT +import android.provider.ContactsContract import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility @@ -32,8 +40,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -49,6 +60,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -75,6 +87,8 @@ import com.infomaniak.swisstransfer.ui.components.LargeButton import com.infomaniak.swisstransfer.ui.components.SwissTransferTextField import com.infomaniak.swisstransfer.ui.components.SwissTransferTopAppBar import com.infomaniak.swisstransfer.ui.components.TopAppBarButtons +import com.infomaniak.swisstransfer.ui.images.AppImages.AppIcons +import com.infomaniak.swisstransfer.ui.images.icons.PersonsCircleAdd import com.infomaniak.swisstransfer.ui.previewparameter.filesPreviewData import com.infomaniak.swisstransfer.ui.screen.main.settings.DownloadLimitOption import com.infomaniak.swisstransfer.ui.screen.main.settings.EmailLanguageOption @@ -88,6 +102,7 @@ import com.infomaniak.swisstransfer.ui.screen.newtransfer.pickfiles.components.T import com.infomaniak.swisstransfer.ui.screen.newtransfer.pickfiles.components.TransferOptionsTypes import com.infomaniak.swisstransfer.ui.screen.newtransfer.pickfiles.components.TransferTypeButtons import com.infomaniak.swisstransfer.ui.screen.newtransfer.pickfiles.components.TransferTypeUi +import com.infomaniak.swisstransfer.ui.theme.Dimens import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.GetSetCallbacks import com.infomaniak.swisstransfer.ui.utils.isApiV2 @@ -197,6 +212,7 @@ fun PickFilesScreen( ), transferOptionsCallbacks = transferOptionsCallbacks, pickFiles = ::pickFiles, + selectContact = pickFilesViewModel::processContactPickerResultUri, exitNewTransfer = { exit() }, onSendButtonClick = { MatomoSwissTransfer.trackNewTransferDataEvent(pickFilesViewModel.selectedTransferTypeFlow.value.dbValue.matomoName) @@ -227,6 +243,7 @@ private fun PickFilesScreen( selectedTransferType: GetSetCallbacks, transferOptionsCallbacks: TransferOptionsCallbacks, pickFiles: () -> Unit, + selectContact: (Uri, Context) -> Unit, exitNewTransfer: () -> Unit, isAwaitingSend: () -> Boolean, onSendButtonClick: () -> Unit, @@ -240,9 +257,7 @@ private fun PickFilesScreen( modifier = Modifier.imePadding(), topBar = { SwissTransferTopAppBar( - titleRes = R.string.importFilesScreenTitle, - actions = { TopAppBarButtons.Close(onClick = { exitNewTransfer() }) } - ) + titleRes = R.string.importFilesScreenTitle, actions = { TopAppBarButtons.Close(onClick = { exitNewTransfer() }) }) }, topButton = { modifier -> SendButton( @@ -264,6 +279,7 @@ private fun PickFilesScreen( emailTextFieldCallbacks = emailTextFieldCallbacks, transferMessageCallbacks = transferMessageCallbacks, shouldShowEmailAddressesFields = { shouldShowEmailAddressesFields }, + selectContact = selectContact, ) TransferOptions(modifier, transferOptionsCallbacks) } @@ -297,6 +313,7 @@ private fun ImportTextFields( emailTextFieldCallbacks: EmailTextFieldCallbacks, transferMessageCallbacks: GetSetCallbacks, shouldShowEmailAddressesFields: () -> Boolean, + selectContact: (Uri, Context) -> Unit, ) { val modifier = horizontalPaddingModifier.fillMaxWidth() @@ -312,7 +329,9 @@ private fun ImportTextFields( ) } - EmailAddressesTextFields(modifier, emailTextFieldCallbacks, shouldShowEmailAddressesFields, textFieldSpacing) + EmailAddressesTextFields( + modifier, emailTextFieldCallbacks, shouldShowEmailAddressesFields, textFieldSpacing, selectContact + ) SwissTransferTextField( modifier = modifier, @@ -331,7 +350,45 @@ private fun EmailAddressesTextFields( emailTextFieldCallbacks: EmailTextFieldCallbacks, shouldShowEmailAddressesFields: () -> Boolean, textFieldSpacing: Dp, + selectContact: (Uri, Context) -> Unit, ) = with(emailTextFieldCallbacks) { + val context = LocalContext.current + + fun buildPickEmailContactIntent(): Intent = Intent(ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI).apply { + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + + fun handlePickedContacts(result: ActivityResult) { + if (result.resultCode != Activity.RESULT_OK) return + + val dataIntent = result.data ?: return + val clipData = dataIntent.clipData + + if (clipData != null) { + for (i in 0 until clipData.itemCount) { + clipData.getItemAt(i).uri?.let { uri -> + selectContact(uri, context) + } + } + return + } + + dataIntent.data?.let { uri -> + selectContact(uri, context) + } + } + + fun launchPickContactSafely(launcher: ActivityResultLauncher) { + try { + launcher.launch(buildPickEmailContactIntent()) + } catch (_: ActivityNotFoundException) { + longToast(R.string.startActivityCantHandleAction) + } + } + + val pickContactLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult(), ::handlePickedContacts) + AnimatedVisibility(visible = shouldShowEmailAddressesFields(), modifier = modifier) { Column(verticalArrangement = Arrangement.spacedBy(textFieldSpacing)) { val isAuthorError = checkEmailError(isAuthor = true) @@ -358,11 +415,24 @@ private fun EmailAddressesTextFields( onValueChange = { recipientEmail.set(it.text) }, isError = isRecipientError, supportingText = getEmailError(isRecipientError), + trailingIcon = { + TrailingButton( + AppIcons.PersonsCircleAdd, onClick = { launchPickContactSafely(pickContactLauncher) }) + }, ) } } } +@Composable +private fun TrailingButton(appIcon: ImageVector, onClick: () -> Unit) { + IconButton(onClick = onClick) { + val (contentDescription, icon) = stringResource(R.string.contentDescriptionButtonSelectContact) to appIcon + + Icon(icon, contentDescription, Modifier.size(Dimens.SmallIconSize)) + } +} + @Composable private fun getEmailError(isError: Boolean): @Composable (() -> Unit)? { val supportingText: @Composable (() -> Unit)? = if (isError) { @@ -493,8 +563,7 @@ enum class PasswordTransferOption( override val imageVector: ImageVector? = null, override val imageVectorResId: Int? = null, ) : SettingOption { - NONE({ stringResource(R.string.settingsOptionNone) }), - ACTIVATED({ stringResource(R.string.settingsOptionActivated) }), + NONE({ stringResource(R.string.settingsOptionNone) }), ACTIVATED({ stringResource(R.string.settingsOptionActivated) }), } @PreviewAllWindows @@ -546,6 +615,7 @@ private fun Preview(@PreviewParameter(UserListPreviewParameterProvider::class) u selectedTransferType = GetSetCallbacks(get = { TransferTypeUi.Mail }, set = {}), transferOptionsCallbacks = transferOptionsCallbacks, pickFiles = {}, + selectContact = { _, _ -> }, exitNewTransfer = {}, onSendButtonClick = {}, isAwaitingSend = { true }, diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/PickFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/PickFilesViewModel.kt index 1256171f3..fa050bc16 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/PickFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/PickFilesViewModel.kt @@ -20,7 +20,10 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer.pickfiles +import android.content.Context +import android.database.Cursor import android.net.Uri +import android.provider.ContactsContract import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -60,6 +63,7 @@ import com.infomaniak.swisstransfer.upload.NewTransferParams import com.infomaniak.swisstransfer.upload.UploadForegroundService import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.CONFLATED @@ -73,6 +77,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import splitties.coroutines.repeatWhileActive import splitties.experimental.ExperimentalSplittiesApi import javax.inject.Inject @@ -128,9 +133,7 @@ class PickFilesViewModel @Inject constructor( } enum class Email : Issue { - AuthorUnspecified, - AuthorInvalid, - NoValidatedRecipients, + AuthorUnspecified, AuthorInvalid, NoValidatedRecipients, } } } @@ -190,8 +193,7 @@ class PickFilesViewModel @Inject constructor( initialValue = FilesDetailsUiState.Success(emptyList()), ) - @OptIn(FlowPreview::class) - importedFilesDebounced = pickedFilesFlow.debounce(50).stateIn( + @OptIn(FlowPreview::class) importedFilesDebounced = pickedFilesFlow.debounce(50).stateIn( scope = viewModelScope, started = SharingStarted.Lazily, initialValue = emptyList(), @@ -250,8 +252,7 @@ class PickFilesViewModel @Inject constructor( } private fun pickedFilesIssues( - maxFilesSize: Long = FileUtils.MAX_FILES_SIZE, - maxFilesCount: Int = FileUtils.MAX_FILE_COUNT + maxFilesSize: Long = FileUtils.MAX_FILES_SIZE, maxFilesCount: Int = FileUtils.MAX_FILE_COUNT ): StateFlow> = UploadForegroundService.pickedFilesFlow.mapSync { pickedFiles -> if (pickedFiles.isEmpty()) { setOf(Issue.Files.NonePicked) @@ -320,8 +321,7 @@ class PickFilesViewModel @Inject constructor( //region Transfer Options val selectedValidityPeriodOption = savedStateHandle.getStateFlow( - key = SELECTED_VALIDITY_PERIOD_KEY, - initialValue = ValidityPeriodOption.THIRTY + key = SELECTED_VALIDITY_PERIOD_KEY, initialValue = ValidityPeriodOption.THIRTY ) val selectedDownloadLimitOption = savedStateHandle.getStateFlow( @@ -404,6 +404,95 @@ class PickFilesViewModel @Inject constructor( is EmailLanguageOption -> selectTransferLanguage(option) } } + + private data class Contact( + val lookupKey: String, + val emails: List, + ) + + fun processContactPickerResultUri( + sessionUri: Uri, + context: Context, + ) { + try { + viewModelScope.launch { contactPickLaunch(sessionUri, context) } + } catch (e: Exception) { + SentryLog.e(TAG, "Error while importing contacts from picker result", e) + } + } + + private suspend fun contactPickLaunch( + sessionUri: Uri, + context: Context, + ) { + val contacts = queryContacts(sessionUri, context) + val newEmails = contacts.values.asSequence().flatMap { it.emails.asSequence() }.toSet() + + updateValidatedRecipientsEmails(newEmails) + } + + private suspend fun queryContacts( + sessionUri: Uri, + context: Context, + ): Map = withContext(ioDispatcher) { + val projection = arrayOf( + ContactsContract.Contacts.LOOKUP_KEY, + ContactsContract.Data.MIMETYPE, + ContactsContract.Data.DATA1, + ) + + val contactsMap = mutableMapOf() + + context.contentResolver.query(sessionUri, projection, null, null, null)?.use { cursor -> + val indices = cursor.contactProjectionIndicesOrNull() + if (indices == null) { + SentryLog.e(TAG, "Invalid projection, missing columns.") + return@use + } + + while (cursor.moveToNext()) { + val lookupKey = cursor.getString(indices.lookupKeyIdx) + val email = cursor.emailOrNull(indices.mimeTypeIdx, indices.data1Idx) ?: continue + + val current = contactsMap[lookupKey] ?: Contact(lookupKey = lookupKey, emails = emptyList()) + contactsMap[lookupKey] = current.copy(emails = current.emails + email) + } + } + contactsMap + } + + private suspend fun updateValidatedRecipientsEmails(newEmails: Set) = withContext(Dispatchers.Main) { + validatedRecipientsEmails = validatedRecipientsEmails + newEmails + } + + private data class ContactProjectionIndices( + val lookupKeyIdx: Int, + val mimeTypeIdx: Int, + val data1Idx: Int, + ) + + private fun Cursor.contactProjectionIndicesOrNull(): ContactProjectionIndices? { + val lookupKeyIdx = getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY) + val mimeTypeIdx = getColumnIndex(ContactsContract.Data.MIMETYPE) + val data1Idx = getColumnIndex(ContactsContract.Data.DATA1) + + return if (lookupKeyIdx == -1 || mimeTypeIdx == -1 || data1Idx == -1) { + null + } else { + ContactProjectionIndices( + lookupKeyIdx = lookupKeyIdx, + mimeTypeIdx = mimeTypeIdx, + data1Idx = data1Idx, + ) + } + } + + private fun Cursor.emailOrNull(mimeTypeIdx: Int, data1Idx: Int): String? { + val mimeType = getString(mimeTypeIdx) + if (mimeType != ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) return null + + return getString(data1Idx) + } //endregion companion object { diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/components/EmailAddressTextField.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/components/EmailAddressTextField.kt index 29af3f739..2524ef484 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/components/EmailAddressTextField.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/pickfiles/components/EmailAddressTextField.kt @@ -83,16 +83,18 @@ import com.infomaniak.swisstransfer.ui.previewparameter.EmailsPreviewParameter import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.GetSetCallbacks + @OptIn(ExperimentalLayoutApi::class) @Composable fun EmailAddressTextField( - modifier: Modifier = Modifier, label: String, initialValue: String, validatedRecipientsEmails: GetSetCallbacks>, onValueChange: (TextFieldValue) -> Unit, + modifier: Modifier = Modifier, isError: Boolean = false, supportingText: (@Composable () -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, ) { val state = remember(validatedRecipientsEmails) { @@ -154,11 +156,13 @@ fun EmailAddressTextField( isError = isError, supportingText = supportingText, textFieldColors = SwissTransferTextFieldDefaults.colors(), + trailingIcon = trailingIcon, ) } ) } + /** * Copied from OutlineTextField so the BasicTextField can have the same spacing as OutlineTextField with a label */ @@ -289,6 +293,7 @@ private fun EmailAddressDecorationBox( isError: Boolean, supportingText: @Composable (() -> Unit)?, textFieldColors: TextFieldColors, + trailingIcon: @Composable (() -> Unit)? = null ) { OutlinedTextFieldDefaults.DecorationBox( value = text, @@ -313,6 +318,7 @@ private fun EmailAddressDecorationBox( supportingText = supportingText, label = { Text(label) }, colors = textFieldColors, + trailingIcon = trailingIcon, ) { OutlinedTextFieldDefaults.Container( enabled = true, diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index b6f960d1c..893971f1b 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -39,6 +39,7 @@ Skjul adgangskode Fjern fil Fjern %s + Åbn kontaktlisten Vis adgangskode Ny overføring Indtast den adgangskode, du har modtaget, for at downloade disse filer. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a0a16b0a2..9e25d7925 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -39,6 +39,7 @@ Passwort verbergen Datei entfernen %s entfernen + Kontaktliste öffnen Passwort anzeigen Neuer Transfer Gib das Passwort ein, das du erhalten hast, um diese Dateien herunterzuladen. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 295ef3fcc..c14dd9369 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -39,6 +39,7 @@ Απόκρυψη κωδικού πρόσβασης Αφαίρεση αρχείου Αφαίρεση %s + Άνοιγμα λίστας επαφών Εμφάνιση κωδικού πρόσβασης Νέα μεταφορά Εισάγαλε τον κωδικό πρόσβασης που σου δόθηκε για να κατεβάσεις αυτά τα αρχεία. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c320dbd94..f15a52f3e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -39,6 +39,7 @@ Ocultar contraseña Eliminar archivo Suprimir %s + Abrir la lista de contactos Mostrar contraseña Nueva transferencia Introduzca la contraseña que se le ha dado para descargar estos archivos. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 9609b95d2..8282ccb95 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -39,6 +39,7 @@ Piilota salasana Poista tiedosto Poista %s + Avaa yhteysluettelo Näytä salasana Uusi siirto Syötä saamasi salasana ladataksesi nämä tiedostot. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ef4fdb9bd..ef5508189 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -39,6 +39,7 @@ Cacher le mot de passe Supprimer le fichier Supprimer %s + Ouvrir la liste des contacts Afficher le mot de passe Nouveau transfert Saisis le mot de passe qui t’a été fourni pour télécharger ces fichiers. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 270f4cd90..51a45644f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -39,6 +39,7 @@ Nascondere la password Rimuovi il file Rimuove %s + Apri l’elenco contatti Mostra password Nuovo trasferimento Inserite la password che vi è stata fornita per scaricare questi file. diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 446d4fed3..1dea6a500 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -39,6 +39,7 @@ Skjul passord Fjern fil Fjern %s + Åpne kontaktlisten Vis passord Ny overføring Skriv inn passordet du har fått for å laste ned disse filene. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 13db06fbe..8f9df21da 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -39,6 +39,7 @@ Wachtwoord verbergen Bestand verwijderen %s verwijderen + Contactenlijst openen Wachtwoord weergeven Nieuwe overdracht Voer het wachtwoord in dat je hebt ontvangen om deze bestanden te downloaden. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 35e047b3f..b546ea993 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -39,6 +39,7 @@ Ukryj hasło Usuń plik Usuń %s + Otwórz listę kontaktów Pokaż hasło Nowy transfer Wprowadź hasło, które otrzymałeś, aby pobrać te pliki. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ffe049add..7656194a0 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -39,6 +39,7 @@ Ocultar palavra-passe Remover ficheiro Remover %s + Abrir a lista de contactos Mostrar palavra-passe Nova transferência Introduz a palavra-passe que te foi fornecida para descarregar estes ficheiros. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 8a98e2f50..468fc58de 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -39,6 +39,7 @@ Dölj lösenord Ta bort fil Ta bort %s + Öppna kontaktlistan Visa lösenord Ny överföring Ange lösenordet du har fått för att ladda ner dessa filer. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e218287fa..615a583a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,6 @@ Matomo Sentry upload_channel_id - Advanced settings Waiting for network Cancel the transfer @@ -44,6 +43,7 @@ Hide password Remove file Remove %s + Open the contacts list Show password New transfer Enter the password you have been given to download these files.