From 48d26beb778d64f3ed54c1c8b5abc1685b49ccbd Mon Sep 17 00:00:00 2001 From: Jeffrey Starke Date: Thu, 17 Apr 2025 11:11:30 -0400 Subject: [PATCH] Add TransferProgressIndicator composable. Adds a composable version of `TransferProgressView`. --- .../TransferProgressIndicator.kt | 134 ++++++++++++++++++ .../transfercontrols/TransferProgressView.kt | 2 + .../stickers/StickerManagementActivityV2.kt | 2 +- .../stickers/StickerManagementViewModelV2.kt | 2 +- .../stickers/StickerPackListItems.kt | 36 +++-- app/src/main/res/drawable/symbol_stop_24.xml | 9 ++ app/src/main/res/values/strings.xml | 8 ++ .../core/ui/compose/ModifierExtensions.kt | 39 +++++ 8 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressIndicator.kt create mode 100644 app/src/main/res/drawable/symbol_stop_24.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressIndicator.kt b/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressIndicator.kt new file mode 100644 index 0000000000..6b47f099b4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressIndicator.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.transfercontrols + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.clickableContainer +import org.thoughtcrime.securesms.R + +/** + * A button that can be used to start, cancel, show progress, and show completion of a data transfer. + */ +@Composable +fun TransferProgressIndicator( + state: TransferProgressState, + modifier: Modifier = Modifier.size(48.dp) +) { + when (state) { + is TransferProgressState.Ready -> StartTransferButton(state, modifier) + is TransferProgressState.InProgress -> ProgressIndicator(state, modifier) + is TransferProgressState.Complete -> CompleteIcon(state, modifier) + } +} + +@Composable +private fun StartTransferButton( + state: TransferProgressState.Ready, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .clickableContainer( + contentDescription = state.startButtonContentDesc, + onClickLabel = state.startButtonOnClickLabel, + onClick = state.onStartClick + ) + ) { + Icon( + painter = state.iconPainter, + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = null, + modifier = Modifier + .matchParentSize() + .padding(12.dp) + ) + } +} + +@Composable +private fun ProgressIndicator( + state: TransferProgressState.InProgress, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .clickableContainer( + contentDescription = null, + onClickLabel = state.cancelButtonOnClickLabel, + onClick = state.onCancelClick + ) + .padding(8.dp) + ) { + Icon( + painter = painterResource(R.drawable.symbol_stop_24), + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = null, + modifier = Modifier + .matchParentSize() + .padding(6.dp) + ) + + CircularProgressIndicator( + progress = { state.progress }, + strokeWidth = 2.dp, + strokeCap = StrokeCap.Round, + trackColor = MaterialTheme.colorScheme.surfaceContainerHighest, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .matchParentSize() + .clearAndSetSemantics { + contentDescription = state.cancelButtonContentDesc + } + ) + } +} + +@Composable +private fun CompleteIcon( + state: TransferProgressState.Complete, + modifier: Modifier = Modifier +) { + Icon( + painter = state.iconPainter, + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = state.iconContentDesc, + modifier = modifier.padding(12.dp) + ) +} + +sealed interface TransferProgressState { + data class Ready( + val iconPainter: Painter, + val startButtonContentDesc: String, + val startButtonOnClickLabel: String, + val onStartClick: () -> Unit + ) : TransferProgressState + + data class InProgress( + val progress: Float, + val cancelButtonContentDesc: String, + val cancelButtonOnClickLabel: String, + val onCancelClick: () -> Unit = {} + ) : TransferProgressState + + data class Complete( + val iconPainter: Painter, + val iconContentDesc: String + ) : TransferProgressState +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressView.kt index 0998e45efb..5b5d77a28b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/transfercontrols/TransferProgressView.kt @@ -15,6 +15,7 @@ import android.graphics.RectF import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View +import androidx.annotation.Discouraged import androidx.core.content.ContextCompat import androidx.core.graphics.withTranslation import org.signal.core.util.logging.Log @@ -24,6 +25,7 @@ import kotlin.math.roundToInt /** * This displays a circular progress around an icon. The icon is either an upload arrow, a download arrow, or a rectangular stop button. */ +@Discouraged("Use TransferProgressIndicator instead.") class TransferProgressView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivityV2.kt index 7674cecfb7..dd6a67c76c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivityV2.kt @@ -279,7 +279,7 @@ private fun AvailableStickersContentPreview() { title = "Bandit the Cat", author = "Agnes Lee", isBlessed = false, - downloadStatus = DownloadStatus.InProgress(progressPercent = 22.0) + downloadStatus = DownloadStatus.InProgress(progress = 0.37f) ), StickerPreviewDataFactory.availablePack( title = "Day by Day", diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt index 1bd5c757f2..5d673accf2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt @@ -52,7 +52,7 @@ data class AvailableStickerPack( ) { sealed class DownloadStatus { data object NotDownloaded : DownloadStatus() - data class InProgress(val progressPercent: Double) : DownloadStatus() + data class InProgress(val progress: Float) : DownloadStatus() data object Downloaded : DownloadStatus() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackListItems.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackListItems.kt index c3fb7fb68c..b46e27ccf3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackListItems.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackListItems.kt @@ -19,16 +19,17 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp -import org.signal.core.ui.compose.IconButtons import org.signal.core.ui.compose.SignalPreview import org.signal.core.ui.compose.theme.SignalTheme import org.signal.core.util.nullIfBlank import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.transfercontrols.TransferProgressIndicator +import org.thoughtcrime.securesms.components.transfercontrols.TransferProgressState import org.thoughtcrime.securesms.compose.GlideImage import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri import org.thoughtcrime.securesms.stickers.AvailableStickerPack.DownloadStatus @@ -53,6 +54,7 @@ fun StickerPackSectionHeader( fun AvailableStickerPackRow( pack: AvailableStickerPack, onStartDownloadClick: () -> Unit = {}, + onCancelDownloadClick: () -> Unit = {}, modifier: Modifier = Modifier ) { Row( @@ -69,15 +71,25 @@ fun AvailableStickerPackRow( modifier = Modifier.weight(1f) ) - // TODO show TransferProgressIndicator based on download state - IconButtons.IconButton( - size = 48.dp, - onClick = onStartDownloadClick, - content = { - Image( - imageVector = ImageVector.vectorResource(id = R.drawable.symbol_arrow_circle_down_24), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), - contentDescription = stringResource(R.string.StickerManagement_accessibility_download_pack, pack.record.title) + TransferProgressIndicator( + state = when (pack.downloadStatus) { + DownloadStatus.NotDownloaded -> TransferProgressState.Ready( + iconPainter = painterResource(id = R.drawable.symbol_arrow_circle_down_24), + startButtonContentDesc = stringResource(R.string.StickerManagement_accessibility_download), + startButtonOnClickLabel = stringResource(R.string.StickerManagement_accessibility_download_pack, pack.record.title), + onStartClick = onStartDownloadClick + ) + + is DownloadStatus.InProgress -> TransferProgressState.InProgress( + progress = pack.downloadStatus.progress, + cancelButtonContentDesc = stringResource(R.string.StickerManagement_accessibility_cancel), + cancelButtonOnClickLabel = stringResource(R.string.StickerManagement_accessibility_cancel_downloading_pack, pack.record.title), + onCancelClick = onCancelDownloadClick + ) + + DownloadStatus.Downloaded -> TransferProgressState.Complete( + iconPainter = painterResource(id = R.drawable.symbol_check_24), + iconContentDesc = stringResource(R.string.StickerManagement_accessibility_downloaded_checkmark, pack.record.title) ) } ) @@ -215,7 +227,7 @@ private fun AvailableStickerPackRowPreviewDownloading() = SignalTheme { title = "Bandit the Cat", author = "Agnes Lee", isBlessed = false, - downloadStatus = DownloadStatus.InProgress(progressPercent = 22.0) + downloadStatus = DownloadStatus.InProgress(progress = 0.37f) ) ) } diff --git a/app/src/main/res/drawable/symbol_stop_24.xml b/app/src/main/res/drawable/symbol_stop_24.xml new file mode 100644 index 0000000000..0c17cdd64a --- /dev/null +++ b/app/src/main/res/drawable/symbol_stop_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b72bffa3b..4e9cd86a04 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2800,8 +2800,16 @@ Signal artist series Download %s sticker pack + + Cancel downloading %s sticker pack + + Downloaded checkmark Drag and drop handle + + Download + + Cancel No stickers installed diff --git a/core-ui/src/main/java/org/signal/core/ui/compose/ModifierExtensions.kt b/core-ui/src/main/java/org/signal/core/ui/compose/ModifierExtensions.kt index fe07bf9c62..750b5a6ba9 100644 --- a/core-ui/src/main/java/org/signal/core/ui/compose/ModifierExtensions.kt +++ b/core-ui/src/main/java/org/signal/core/ui/compose/ModifierExtensions.kt @@ -5,10 +5,18 @@ package org.signal.core.ui.compose +import androidx.compose.foundation.Indication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import org.signal.core.ui.R @@ -21,3 +29,34 @@ fun Modifier.horizontalGutters( ): Modifier { return padding(horizontal = gutterSize) } + +/** + * Configures a component to be clickable within its bounds and show a default indication when pressed. + * + * This modifier is designed for use on container components, making it easier to create a clickable container with proper accessibility configuration. + */ +@Composable +fun Modifier.clickableContainer( + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + indication: Indication = ripple(bounded = false), + enabled: Boolean = true, + contentDescription: String?, + onClickLabel: String, + role: Role? = null, + onClick: () -> Unit +): Modifier = clickable( + interactionSource = interactionSource, + indication = indication, + enabled = enabled, + onClickLabel = onClickLabel, + role = role, + onClick = onClick +).then( + if (contentDescription != null) { + Modifier.semantics(mergeDescendants = true) { + this.contentDescription = contentDescription + } + } else { + Modifier + } +)