Add sticker details.

This commit is contained in:
Michelle Tang
2026-09-23 16:12:23 -04:00
parent cd151d083c
commit 9d0f337d27
6 changed files with 175 additions and 4 deletions
@@ -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
}
@@ -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(
@@ -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
}
@@ -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
}
}
@@ -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 = {}
)
}
}
+2
View File
@@ -2874,6 +2874,8 @@
<string name="StickerPackPreviewActivity_remove">Uninstall</string>
<string name="StickerPackPreviewActivity_stickers">Stickers</string>
<string name="StickerPackPreviewActivity_failed_to_load_sticker_pack">Failed to load sticker pack</string>
<!-- Bottom sheet describing a sticker. First placeholder is the title of the sticker pack and second is the author -->
<string name="StickerPreviewBottomSheet__title_author">%1$s · %2$s</string>
<!-- Menu option to save a debug log file to disk. -->
<string name="SubmitDebugLogActivity_save">Save</string>