From 947ab7d48b18146c7207be04b48cb84aced68ef3 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 18 Apr 2024 13:11:48 -0300 Subject: [PATCH] Implement skeleton for backup sheets. --- .../backup/v2/ui/BackupAlertBottomSheet.kt | 289 ++++++++++++++++++ .../backup/v2/ui/BackupsIconColors.kt | 45 +++ .../backup/v2/ui/status/BackupStatus.kt | 58 +--- .../src/main/java/org/signal/core/ui/Icons.kt | 57 ++++ 4 files changed, 402 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupsIconColors.kt create mode 100644 core-ui/src/main/java/org/signal/core/ui/Icons.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt new file mode 100644 index 0000000000..a0bb482a7d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt @@ -0,0 +1,289 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.ui + +import android.os.Parcelable +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.os.BundleCompat +import androidx.core.os.bundleOf +import kotlinx.parcelize.Parcelize +import org.signal.core.ui.BottomSheets +import org.signal.core.ui.Buttons +import org.signal.core.ui.Icons +import org.signal.core.ui.Previews +import org.signal.core.ui.SignalPreview +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment + +/** + * Notifies the user of an issue with their backup. + */ +class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() { + + companion object { + private const val ARG_ALERT = "alert" + + fun create(backupAlert: BackupAlert): BackupAlertBottomSheet { + return BackupAlertBottomSheet().apply { + arguments = bundleOf(ARG_ALERT to backupAlert) + } + } + } + + private val backupAlert: BackupAlert by lazy(LazyThreadSafetyMode.NONE) { + BundleCompat.getParcelable(requireArguments(), ARG_ALERT, BackupAlert::class.java)!! + } + + @Composable + override fun SheetContent() { + BackupAlertSheetContent( + backupAlert = backupAlert, + onPrimaryActionClick = this::performPrimaryAction, + onSecondaryActionClick = this::performSecondaryAction + ) + } + + @Stable + private fun performPrimaryAction() { + when (backupAlert) { + BackupAlert.GENERIC -> { + // TODO [message-backups] -- Back up now + } + BackupAlert.PAYMENT_PROCESSING -> { + // TODO [message-backups] -- Silence + } + BackupAlert.MEDIA_BACKUPS_ARE_OFF -> { + // TODO [message-backups] -- Download media now + } + BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> { + // TODO [message-backups] -- Download media now + } + } + + dismissAllowingStateLoss() + } + + @Stable + private fun performSecondaryAction() { + when (backupAlert) { + BackupAlert.GENERIC -> { + // TODO [message-backups] - Dismiss and notify later + } + BackupAlert.PAYMENT_PROCESSING -> error("PAYMENT_PROCESSING state does not support a secondary action.") + BackupAlert.MEDIA_BACKUPS_ARE_OFF -> { + // TODO [message-backups] - Silence and remind on last day + } + BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> { + // TODO [message-backups] - Silence forever + } + } + + dismissAllowingStateLoss() + } +} + +@Composable +private fun BackupAlertSheetContent( + backupAlert: BackupAlert, + onPrimaryActionClick: () -> Unit, + onSecondaryActionClick: () -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) + ) { + BottomSheets.Handle() + + Spacer(modifier = Modifier.size(26.dp)) + + val iconColors = rememberBackupsIconColors(backupAlert = backupAlert) + Icons.BrushedForeground( + painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] final asset + contentDescription = null, + foregroundBrush = iconColors.foreground, + modifier = Modifier + .size(88.dp) + .background(color = iconColors.background, shape = CircleShape) + .padding(20.dp) + ) + + Text( + text = stringResource(id = rememberTitleResource(backupAlert = backupAlert)), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) + ) + + when (backupAlert) { + BackupAlert.GENERIC -> GenericBody() + BackupAlert.PAYMENT_PROCESSING -> PaymentProcessingBody() + BackupAlert.MEDIA_BACKUPS_ARE_OFF -> MediaBackupsAreOffBody() + BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> MediaWillBeDeletedTodayBody() + } + + val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert) + val padBottom = if (secondaryActionResource > 0) 16.dp else 56.dp + + Buttons.LargeTonal( + onClick = onPrimaryActionClick, + modifier = Modifier + .defaultMinSize(minWidth = 220.dp) + .padding(top = 60.dp, bottom = padBottom) + ) { + Text(text = stringResource(id = rememberPrimaryActionResource(backupAlert = backupAlert))) + } + + if (secondaryActionResource > 0) { + TextButton(onClick = onSecondaryActionClick, modifier = Modifier.padding(bottom = 32.dp)) { + Text(text = stringResource(id = secondaryActionResource)) + } + } + } +} + +@Composable +private fun GenericBody() { + Text(text = "TODO") +} + +@Composable +private fun PaymentProcessingBody() { + Text(text = "TODO") +} + +@Composable +private fun MediaBackupsAreOffBody() { + Text(text = "TODO") +} + +@Composable +private fun MediaWillBeDeletedTodayBody() { + Text(text = "TODO") +} + +@Composable +private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors { + return remember(backupAlert) { + when (backupAlert) { + BackupAlert.GENERIC, BackupAlert.PAYMENT_PROCESSING -> BackupsIconColors.Warning + BackupAlert.MEDIA_BACKUPS_ARE_OFF, BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> BackupsIconColors.Error + } + } +} + +@Composable +@StringRes +private fun rememberTitleResource(backupAlert: BackupAlert): Int { + return remember(backupAlert) { + when (backupAlert) { + BackupAlert.GENERIC -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy + BackupAlert.PAYMENT_PROCESSING -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy + BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy + BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy + } + } +} + +@Composable +private fun rememberPrimaryActionResource(backupAlert: BackupAlert): Int { + return remember(backupAlert) { + when (backupAlert) { + BackupAlert.GENERIC -> android.R.string.ok // TODO [message-backups] -- Finalized copy + BackupAlert.PAYMENT_PROCESSING -> android.R.string.ok // TODO [message-backups] -- Finalized copy + BackupAlert.MEDIA_BACKUPS_ARE_OFF -> android.R.string.ok // TODO [message-backups] -- Finalized copy + BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> android.R.string.ok // TODO [message-backups] -- Finalized copy + } + } +} + +@Composable +private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int { + return remember(backupAlert) { + when (backupAlert) { + BackupAlert.GENERIC -> android.R.string.cancel // TODO [message-backups] -- Finalized copy + BackupAlert.PAYMENT_PROCESSING -> -1 + BackupAlert.MEDIA_BACKUPS_ARE_OFF -> android.R.string.cancel // TODO [message-backups] -- Finalized copy + BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> android.R.string.cancel // TODO [message-backups] -- Finalized copy + } + } +} + +@SignalPreview +@Composable +private fun BackupAlertSheetContentPreviewGeneric() { + Previews.BottomSheetPreview { + BackupAlertSheetContent( + backupAlert = BackupAlert.GENERIC, + onPrimaryActionClick = {}, + onSecondaryActionClick = {} + ) + } +} + +@SignalPreview +@Composable +private fun BackupAlertSheetContentPreviewPayment() { + Previews.BottomSheetPreview { + BackupAlertSheetContent( + backupAlert = BackupAlert.PAYMENT_PROCESSING, + onPrimaryActionClick = {}, + onSecondaryActionClick = {} + ) + } +} + +@SignalPreview +@Composable +private fun BackupAlertSheetContentPreviewMedia() { + Previews.BottomSheetPreview { + BackupAlertSheetContent( + backupAlert = BackupAlert.MEDIA_BACKUPS_ARE_OFF, + onPrimaryActionClick = {}, + onSecondaryActionClick = {} + ) + } +} + +@SignalPreview +@Composable +private fun BackupAlertSheetContentPreviewDelete() { + Previews.BottomSheetPreview { + BackupAlertSheetContent( + backupAlert = BackupAlert.MEDIA_WILL_BE_DELETED_TODAY, + onPrimaryActionClick = {}, + onSecondaryActionClick = {} + ) + } +} + +@Parcelize +enum class BackupAlert : Parcelable { + GENERIC, + PAYMENT_PROCESSING, + MEDIA_BACKUPS_ARE_OFF, + MEDIA_WILL_BE_DELETED_TODAY +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupsIconColors.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupsIconColors.kt new file mode 100644 index 0000000000..16e8396066 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupsIconColors.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.ui + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor + +sealed interface BackupsIconColors { + @get:Composable + val foreground: Brush + + @get:Composable + val background: Color + + object Normal : BackupsIconColors { + override val foreground: Brush + @Composable get() = remember { + Brush.linearGradient( + colors = listOf(Color(0xFF316ED0), Color(0xFF558BE2)), + start = Offset(x = 0f, y = Float.POSITIVE_INFINITY), + end = Offset(x = Float.POSITIVE_INFINITY, y = 0f) + ) + } + + override val background: Color @Composable get() = MaterialTheme.colorScheme.primaryContainer + } + + object Warning : BackupsIconColors { + override val foreground: Brush @Composable get() = SolidColor(Color(0xFFC86600)) + override val background: Color @Composable get() = Color(0xFFF9E4B6) + } + + object Error : BackupsIconColors { + override val foreground: Brush @Composable get() = SolidColor(MaterialTheme.colorScheme.error) + override val background: Color @Composable get() = Color(0xFFFFD9D9) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatus.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatus.kt index d71de68a82..023b295a46 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatus.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatus.kt @@ -17,30 +17,23 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.signal.core.ui.Buttons +import org.signal.core.ui.Icons import org.signal.core.ui.Previews import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors import kotlin.math.max import kotlin.math.min @@ -63,19 +56,13 @@ fun BackupStatus( .padding(14.dp) ) { val foreground: Brush = data.iconColors.foreground - Icon( + Icons.BrushedForeground( painter = painterResource(id = data.iconRes), contentDescription = null, + foregroundBrush = foreground, modifier = Modifier .background(color = data.iconColors.background, shape = CircleShape) .padding(8.dp) - .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } - .drawWithCache { - onDrawWithContent { - drawContent() - drawRect(foreground, blendMode = BlendMode.SrcAtop) - } - } ) Column( @@ -92,7 +79,9 @@ fun BackupStatus( LinearProgressIndicator( progress = data.progress, strokeCap = StrokeCap.Round, - modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp) + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp) ) } @@ -152,7 +141,7 @@ sealed interface BackupStatusData { @get:StringRes val titleRes: Int - val iconColors: IconColors + val iconColors: BackupsIconColors @get:StringRes val actionRes: Int get() = NONE @@ -168,7 +157,7 @@ sealed interface BackupStatusData { object CouldNotCompleteBackup : BackupStatusData { override val iconRes: Int = R.drawable.symbol_backup_light override val titleRes: Int = R.string.default_error_msg - override val iconColors: IconColors = IconColors.Warning + override val iconColors: BackupsIconColors = BackupsIconColors.Warning } /** @@ -177,7 +166,7 @@ sealed interface BackupStatusData { object NotEnoughFreeSpace : BackupStatusData { override val iconRes: Int = R.drawable.symbol_backup_light override val titleRes: Int = R.string.default_error_msg - override val iconColors: IconColors = IconColors.Warning + override val iconColors: BackupsIconColors = BackupsIconColors.Warning override val actionRes: Int = R.string.registration_activity__skip } @@ -190,7 +179,7 @@ sealed interface BackupStatusData { val status: Status = Status.NONE ) : BackupStatusData { override val iconRes: Int = R.drawable.symbol_backup_light - override val iconColors: IconColors = IconColors.Normal + override val iconColors: BackupsIconColors = BackupsIconColors.Normal override val titleRes: Int = when (status) { Status.NONE -> R.string.default_error_msg @@ -215,31 +204,6 @@ sealed interface BackupStatusData { } } - sealed interface IconColors { - @get:Composable - val foreground: Brush - - @get:Composable - val background: Color - - object Normal : IconColors { - override val foreground: Brush @Composable get() = remember { - Brush.linearGradient( - colors = listOf(Color(0xFF316ED0), Color(0xFF558BE2)), - start = Offset(x = 0f, y = Float.POSITIVE_INFINITY), - end = Offset(x = Float.POSITIVE_INFINITY, y = 0f) - ) - } - - override val background: Color @Composable get() = MaterialTheme.colorScheme.primaryContainer - } - - object Warning : IconColors { - override val foreground: Brush @Composable get() = SolidColor(Color(0xFFC86600)) - override val background: Color @Composable get() = Color(0xFFF9E4B6) - } - } - /** * Describes the status of an in-progress media download session. */ diff --git a/core-ui/src/main/java/org/signal/core/ui/Icons.kt b/core-ui/src/main/java/org/signal/core/ui/Icons.kt new file mode 100644 index 0000000000..397871cd41 --- /dev/null +++ b/core-ui/src/main/java/org/signal/core/ui/Icons.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.ui + +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview + +object Icons { + /** + * Icon that takes a Brush instead of a Color for its foreground + */ + @Composable + fun BrushedForeground( + painter: Painter, + contentDescription: String?, + foregroundBrush: Brush, + modifier: Modifier = Modifier + ) { + Icon( + painter = painter, + contentDescription = contentDescription, + modifier = modifier + .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } + .drawWithCache { + onDrawWithContent { + drawContent() + drawRect(foregroundBrush, blendMode = BlendMode.SrcAtop) + } + } + ) + } +} + +@Preview +@Composable +private fun BrushedForegroundPreview() { + Previews.Preview { + Icons.BrushedForeground( + painter = painterResource(id = android.R.drawable.ic_menu_camera), + contentDescription = null, + foregroundBrush = Brush.linearGradient(listOf(Color.Red, Color.Blue)) + ) + } +}