From 9d0f337d270c8662faee761c4398abc4716575e5 Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Tue, 22 Sep 2026 17:14:19 -0400 Subject: [PATCH] Add sticker details. --- .../preview/StickerPackPreviewAction.kt | 2 + .../preview/StickerPackPreviewActivityV2.kt | 68 ++++++++++++- .../preview/StickerPackPreviewEvent.kt | 4 + .../preview/StickerPackPreviewViewModelV2.kt | 8 ++ .../preview/StickerPreviewBottomSheet.kt | 95 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 6 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPreviewBottomSheet.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewAction.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewAction.kt index 5a7000aba4..b7afb31e0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewAction.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewAction.kt @@ -6,6 +6,7 @@ package org.thoughtcrime.securesms.stickers.preview import org.thoughtcrime.securesms.database.model.StickerPackParams +import org.thoughtcrime.securesms.stickers.StickerManifest /** * Actions that happen in [StickerPackPreviewActivityV2] @@ -13,6 +14,7 @@ import org.thoughtcrime.securesms.database.model.StickerPackParams sealed interface StickerPackPreviewAction { data class SendPack(val params: StickerPackParams) : StickerPackPreviewAction data class ShareExternally(val params: StickerPackParams) : StickerPackPreviewAction + data class SendSticker(val sticker: StickerManifest.Sticker) : StickerPackPreviewAction data object LinkCopied : StickerPackPreviewAction data object PackUnavailable : StickerPackPreviewAction } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewActivityV2.kt index 4b00db63b2..a3ddbe55fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewActivityV2.kt @@ -12,6 +12,7 @@ import androidx.activity.compose.setContent import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Image import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -84,12 +85,14 @@ import org.thoughtcrime.securesms.database.model.StickerPackId import org.thoughtcrime.securesms.database.model.StickerPackKey import org.thoughtcrime.securesms.database.model.StickerPackParams import org.thoughtcrime.securesms.sharing.MultiShareArgs +import org.thoughtcrime.securesms.stickers.StickerLocator import org.thoughtcrime.securesms.stickers.StickerManifest import org.thoughtcrime.securesms.stickers.StickerPreviewDataFactory import org.thoughtcrime.securesms.stickers.StickerRemoteUri import org.thoughtcrime.securesms.stickers.StickerUrl import org.thoughtcrime.securesms.stickers.preview.StickerPackPreviewUiState.ContentState import org.thoughtcrime.securesms.stickers.preview.StickerPackPreviewUiState.UserPrompt +import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.viewModel import java.text.NumberFormat import kotlin.jvm.optionals.getOrElse @@ -147,6 +150,7 @@ class StickerPackPreviewActivityV2 : PassphraseRequiredActivity() { Toast.makeText(this, R.string.StickerPackPreviewActivity_failed_to_load_sticker_pack, Toast.LENGTH_SHORT).show() onBackPressedDispatcher.onBackPressed() } + is StickerPackPreviewAction.SendSticker -> openStickerShareSheet(action.sticker) } } @@ -164,6 +168,24 @@ class StickerPackPreviewActivityV2 : PassphraseRequiredActivity() { ) } + private fun openStickerShareSheet(sticker: StickerManifest.Sticker) { + val uri = sticker.uri.orNull() ?: return + + MultiselectForwardFragment.showBottomSheet( + supportFragmentManager = supportFragmentManager, + multiselectForwardFragmentArgs = MultiselectForwardFragmentArgs( + multiShareArgs = listOf( + MultiShareArgs.Builder() + .withDataUri(uri) + .withDataType(sticker.contentType?.takeIf { it.isNotBlank() } ?: MediaUtil.IMAGE_WEBP) + .withStickerLocator(StickerLocator(sticker.packId, sticker.packKey, sticker.id, sticker.emoji)) + .build() + ), + title = R.string.StickerManagement_share_sheet_title + ) + ) + } + private fun openSystemShareSheet(params: StickerPackParams) { val shareIntent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" @@ -280,6 +302,15 @@ private fun StickerPackPreviewScreen( ) } + if (uiState.userPrompt is UserPrompt.PreviewSticker && stickerManifest != null) { + StickerPreviewSheet( + stickerManifest, + uiState.userPrompt.sticker, + loadedState.isPackInstalled, + onEvent + ) + } + when (uiState.contentState) { is ContentState.Loading -> { Box( @@ -380,7 +411,8 @@ private fun StickerPackPreviewContent( ) { item -> StickerImage( sticker = item, - modifier = Modifier.size(96.dp) + modifier = Modifier.size(96.dp), + onClick = { onEvent(StickerPackPreviewEvent.StickerClicked(item)) } ) } } @@ -542,6 +574,29 @@ private fun StickerPackShareSheet( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun StickerPreviewSheet( + stickerManifest: StickerManifest, + sticker: StickerManifest.Sticker, + isPackInstalled: Boolean, + onEvent: (StickerPackPreviewEvent) -> Unit +) { + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + + BottomSheets.BottomSheet( + sheetState = sheetState, + onDismissRequest = { onEvent(StickerPackPreviewEvent.ShareSheetDismissed) } + ) { + StickerPreviewSheetContent( + stickerManifest, + sticker, + isPackInstalled + ) { sheetState.dismissWithAnimation(scope, onComplete = { onEvent(StickerPackPreviewEvent.StickerSent(sticker)) }) } + } +} + @Composable private fun StickerPackShareSheetContent( params: StickerPackParams, @@ -627,15 +682,20 @@ private fun StickerPackShareSheetContent( } @Composable -private fun StickerImage( +fun StickerImage( modifier: Modifier = Modifier, - sticker: StickerManifest.Sticker + sticker: StickerManifest.Sticker, + onClick: (() -> Unit)? = null ) { if (!LocalInspectionMode.current) { GlideImage( model = sticker.imageModel, enableApngAnimation = true, - modifier = modifier + modifier = if (onClick != null) { + modifier.clickable(onClick = { onClick() }) + } else { + modifier + } ) } else { Image( diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewEvent.kt index 0932924868..b41edfeb45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewEvent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewEvent.kt @@ -5,6 +5,8 @@ package org.thoughtcrime.securesms.stickers.preview +import org.thoughtcrime.securesms.stickers.StickerManifest + /** * Events that happen in [StickerPackPreviewActivityV2] */ @@ -19,4 +21,6 @@ sealed interface StickerPackPreviewEvent { data object ShareSheetDismissed : StickerPackPreviewEvent data object CopyLinkClicked : StickerPackPreviewEvent data object ShareExternallyClicked : StickerPackPreviewEvent + data class StickerClicked(val sticker: StickerManifest.Sticker) : StickerPackPreviewEvent + data class StickerSent(val sticker: StickerManifest.Sticker) : StickerPackPreviewEvent } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewViewModelV2.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewViewModelV2.kt index 05a0277b12..9085bb31a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewViewModelV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPackPreviewViewModelV2.kt @@ -99,6 +99,13 @@ class StickerPackPreviewViewModelV2( showPrompt(null) internalActions.send(StickerPackPreviewAction.ShareExternally(params)) } + + is StickerPackPreviewEvent.StickerClicked -> showPrompt(UserPrompt.PreviewSticker(event.sticker)) + + is StickerPackPreviewEvent.StickerSent -> { + showPrompt(null) + internalActions.send(StickerPackPreviewAction.SendSticker(event.sticker)) + } } } @@ -179,5 +186,6 @@ data class StickerPackPreviewUiState( sealed interface UserPrompt { data object ConfirmRemovePack : UserPrompt data object ShareStickerPack : UserPrompt + data class PreviewSticker(val sticker: StickerManifest.Sticker) : UserPrompt } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPreviewBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPreviewBottomSheet.kt new file mode 100644 index 0000000000..293d4f2fbc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/preview/StickerPreviewBottomSheet.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.stickers.preview + +import android.net.Uri +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Dividers +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.Rows +import org.signal.core.ui.compose.SignalIcons +import org.signal.core.util.orNull +import org.signal.core.util.toOptional +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.stickers.StickerManifest +import org.thoughtcrime.securesms.stickers.StickerPreviewDataFactory + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StickerPreviewSheetContent( + stickerManifest: StickerManifest, + sticker: StickerManifest.Sticker, + isPackInstalled: Boolean, + onForwardClick: () -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + StickerImage( + sticker = sticker, + modifier = Modifier + .padding(vertical = 24.dp) + .size(144.dp) + ) + + val title = stickerManifest.title.orNull() ?: stringResource(R.string.StickerPackPreviewActivity_untitled) + val author = stickerManifest.author.orNull() ?: stringResource(R.string.StickerManagement_author_unknown) + + Text( + text = stringResource(R.string.StickerPreviewBottomSheet__title_author, title, author), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 16.dp) + ) + + if (isPackInstalled && sticker.uri.isPresent) { + Dividers.Default() + + Rows.TextRow( + text = stringResource(R.string.StickerManagement_menu_send_pack), + icon = SignalIcons.Forward.imageVector, + onClick = onForwardClick + ) + } + } +} + +@DayNightPreviews +@Composable +private fun StickerPackShareSheetContentPreview() { + Previews.BottomSheetContentPreview { + val cover = StickerManifest.Sticker("packId0", "packKey0", 0, "๐Ÿ‘", null, Uri.EMPTY) + + StickerPreviewSheetContent( + stickerManifest = StickerManifest( + cover.packId, + cover.packKey, + "Misbrands".toOptional(), + "Sticker Pack Author".toOptional(), + cover.toOptional(), + StickerPreviewDataFactory.manifestStickers(33) + ), + sticker = cover, + isPackInstalled = true, + onForwardClick = {} + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4c48392c6b..cafa953eb5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2874,6 +2874,8 @@ Uninstall Stickers Failed to load sticker pack + + %1$s ยท %2$s Save