Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -197,6 +212,7 @@ fun PickFilesScreen(
),
transferOptionsCallbacks = transferOptionsCallbacks,
pickFiles = ::pickFiles,
selectContact = pickFilesViewModel::processContactPickerResultUri,
exitNewTransfer = { exit() },
onSendButtonClick = {
MatomoSwissTransfer.trackNewTransferDataEvent(pickFilesViewModel.selectedTransferTypeFlow.value.dbValue.matomoName)
Expand Down Expand Up @@ -227,6 +243,7 @@ private fun PickFilesScreen(
selectedTransferType: GetSetCallbacks<TransferTypeUi>,
transferOptionsCallbacks: TransferOptionsCallbacks,
pickFiles: () -> Unit,
selectContact: (Uri, Context) -> Unit,
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The selectContact callback signature includes Context, which propagates Android framework types through the composable tree and makes the API harder to reuse/test. If the ViewModel injects @ApplicationContext (see comment in PickFilesViewModel), this callback can likely be simplified to only pass the picked Uri (or a list of Uris).

Suggested change
selectContact: (Uri, Context) -> Unit,
selectContact: (Uri) -> Unit,

Copilot uses AI. Check for mistakes.
exitNewTransfer: () -> Unit,
isAwaitingSend: () -> Boolean,
onSendButtonClick: () -> Unit,
Expand All @@ -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(
Expand All @@ -264,6 +279,7 @@ private fun PickFilesScreen(
emailTextFieldCallbacks = emailTextFieldCallbacks,
transferMessageCallbacks = transferMessageCallbacks,
shouldShowEmailAddressesFields = { shouldShowEmailAddressesFields },
selectContact = selectContact,
)
TransferOptions(modifier, transferOptionsCallbacks)
}
Expand Down Expand Up @@ -297,6 +313,7 @@ private fun ImportTextFields(
emailTextFieldCallbacks: EmailTextFieldCallbacks,
transferMessageCallbacks: GetSetCallbacks<String>,
shouldShowEmailAddressesFields: () -> Boolean,
selectContact: (Uri, Context) -> Unit,
) {
val modifier = horizontalPaddingModifier.fillMaxWidth()

Expand All @@ -312,7 +329,9 @@ private fun ImportTextFields(
)
}

EmailAddressesTextFields(modifier, emailTextFieldCallbacks, shouldShowEmailAddressesFields, textFieldSpacing)
EmailAddressesTextFields(
modifier, emailTextFieldCallbacks, shouldShowEmailAddressesFields, textFieldSpacing, selectContact
)

SwissTransferTextField(
modifier = modifier,
Expand All @@ -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<Intent>) {
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)
Expand All @@ -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))
}
}
Comment thread
aymericmariaux marked this conversation as resolved.

@Composable
private fun getEmailError(isError: Boolean): @Composable (() -> Unit)? {
val supportingText: @Composable (() -> Unit)? = if (isError) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 },
Expand Down
Loading
Loading