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