+ {pack.cover && (

- ))}
- {Array.from({ length: placeholders }, (_, index) => {
- return (
-
+
+ {pack.isBlessed && (
+
+
+
+ )}
+
+
+
{pack.author}
+
+ {i18n('icu:stickers--StickerPreview--StickerCount', {
+ count: pack.stickerCount,
+ })}
+
+
+
+
+ {pack.status === 'installed' && (
+
- );
- })}
+ )}
+
+
+ {pack.stickers.map(({ emoji, id, url }) => (
+

+ ))}
+ {Array.from({ length: placeholders }, (_, index) => {
+ return (
+
+ );
+ })}
+
);
}
+function isInstallFooterVisible(
+ pack: StickerPackType | undefined
+): pack is StickerPackType {
+ return Boolean(
+ pack &&
+ pack.status != null &&
+ pack.status !== 'error' &&
+ pack.status !== 'installed'
+ );
+}
+
export const StickerPreviewModal = memo(function StickerPreviewModalInner({
closeStickerPackPreview,
downloadStickerPack,
@@ -94,6 +174,7 @@ export const StickerPreviewModal = memo(function StickerPreviewModalInner({
installStickerPack,
onClose,
pack,
+ showToast,
uninstallStickerPack,
}: Props) {
const [confirmingUninstall, setConfirmingUninstall] = useState(false);
@@ -133,8 +214,6 @@ export const StickerPreviewModal = memo(function StickerPreviewModalInner({
onClose?.();
}, [closeStickerPackPreview, onClose, pack]);
- const isInstalled = Boolean(pack && pack.status === 'installed');
-
const handleInstall = useCallback(() => {
if (!pack) {
return;
@@ -148,9 +227,7 @@ export const StickerPreviewModal = memo(function StickerPreviewModalInner({
} else {
installStickerPack(pack.id, pack.key, { actionSource: 'ui' });
}
-
- handleClose();
- }, [downloadStickerPack, installStickerPack, handleClose, pack]);
+ }, [downloadStickerPack, installStickerPack, pack]);
const handleStartUninstall = useCallback(() => {
setConfirmingUninstall(true);
@@ -164,53 +241,44 @@ export const StickerPreviewModal = memo(function StickerPreviewModalInner({
setConfirmingUninstall(false);
}, [uninstallStickerPack, setConfirmingUninstall, pack]);
+ const handleCopyLink = useCallback(() => {
+ if (!pack) {
+ return;
+ }
+
+ const link = artAddStickersRoute
+ .toWebUrl({
+ packId: pack.id,
+ packKey: fromBase64PackKeyToHex(pack.key),
+ })
+ .toString();
+ drop(window.navigator.clipboard.writeText(link));
+ showToast({ toastType: ToastType.CopiedStickerPackLink });
+ }, [pack, showToast]);
+
return (
<>
-
+
{i18n('icu:stickers--StickerPreview--Title')}
- {renderBody({ pack, i18n })}
+
+ {renderBody({ pack, i18n, handleCopyLink, handleStartUninstall })}
+
- {pack != null && pack.status != null && pack.status !== 'error' && (
-
-
-
- {pack.isBlessed && (
-
- {' '}
-
-
- )}
-
- {pack.author}
-
+ {isInstallFooterVisible(pack) && (
+
+ {i18n('icu:stickers--StickerPreview--Install')}
+
)}
-
- {isInstalled ? (
-
- {i18n('icu:stickers--StickerManager--Uninstall')}
-
- ) : (
-
- {i18n('icu:stickers--StickerManager--Install')}
-
- )}
-
diff --git a/ts/state/ducks/stickers.preload.ts b/ts/state/ducks/stickers.preload.ts
index 3851d6055c..2d686ecea1 100644
--- a/ts/state/ducks/stickers.preload.ts
+++ b/ts/state/ducks/stickers.preload.ts
@@ -397,14 +397,32 @@ export function reducer(
action: Readonly
): StickersStateType {
if (action.type === 'stickers/STICKER_PACK_ADDED') {
- // ts complains due to `stickers: {}` being overridden by the payload
- // but without full confidence that that's the case, `any` and ignore
- // oxlint-disable-next-line typescript/no-explicit-any
- const { payload } = action as any;
- const newPack = {
- stickers: {},
- ...payload,
- };
+ const { payload } = action;
+
+ // When going from an ephemeral (previewed) pack to installed state,
+ // copy over ephemeral pack props in memory
+ const oldPack = state.packs[payload.id];
+ const isInstallingPack =
+ oldPack !== undefined &&
+ payload.attemptedStatus === 'installed' &&
+ payload.status === 'pending';
+ let newPack: StickerPackDBType;
+ if (isInstallingPack) {
+ newPack = {
+ ...payload,
+ stickerCount: oldPack.stickerCount,
+ title: payload.title === '' ? oldPack.title : payload.title,
+ author: payload.author === '' ? oldPack.author : payload.author,
+ // TODO: Ephemeral stickers are stored at a different path then downloaded stickers
+ // so we can't reuse them
+ stickers: payload.stickers ?? {},
+ };
+ } else {
+ newPack = {
+ ...payload,
+ stickers: payload.stickers ?? {},
+ };
+ }
return {
...state,
diff --git a/ts/state/smart/ConversationPanel.preload.tsx b/ts/state/smart/ConversationPanel.preload.tsx
index 654b2e1110..3875c74c3a 100644
--- a/ts/state/smart/ConversationPanel.preload.tsx
+++ b/ts/state/smart/ConversationPanel.preload.tsx
@@ -42,6 +42,7 @@ import { SmartPinnedMessagesPanel } from './PinnedMessagesPanel.preload.tsx';
import { SmartMiniPlayer } from './MiniPlayer.preload.tsx';
import { SmartGroupMemberLabelEditor } from './GroupMemberLabelEditor.preload.tsx';
import { useNavActions } from '../ducks/nav.std.ts';
+import { ErrorBoundary } from '../../components/ErrorBoundary.dom.tsx';
const log = createLogger('ConversationPanel');
@@ -440,7 +441,11 @@ function PanelElement({
}
if (panel.type === PanelType.StickerManager) {
- return ;
+ return (
+
+
+
+ );
}
log.warn(toLogFormat(missingCaseError(panel.type)));
diff --git a/ts/state/smart/StickerManager.preload.tsx b/ts/state/smart/StickerManager.preload.tsx
index a3e79284e3..ff7dd0d68c 100644
--- a/ts/state/smart/StickerManager.preload.tsx
+++ b/ts/state/smart/StickerManager.preload.tsx
@@ -13,6 +13,7 @@ import {
} from '../selectors/stickers.std.ts';
import { useStickersActions } from '../ducks/stickers.preload.ts';
import { useGlobalModalActions } from '../ducks/globalModals.preload.ts';
+import { useToastActions } from '../ducks/toast.preload.ts';
export const SmartStickerManager = memo(function SmartStickerManager() {
const i18n = useSelector(getIntl);
@@ -24,6 +25,7 @@ export const SmartStickerManager = memo(function SmartStickerManager() {
const { downloadStickerPack, installStickerPack, uninstallStickerPack } =
useStickersActions();
const { closeStickerPackPreview } = useGlobalModalActions();
+ const { showToast } = useToastActions();
return (
);
});
diff --git a/ts/state/smart/StickerPreviewModal.preload.tsx b/ts/state/smart/StickerPreviewModal.preload.tsx
index e651353df7..fbd3478876 100644
--- a/ts/state/smart/StickerPreviewModal.preload.tsx
+++ b/ts/state/smart/StickerPreviewModal.preload.tsx
@@ -12,6 +12,7 @@ import {
} from '../selectors/stickers.std.ts';
import { useStickersActions } from '../ducks/stickers.preload.ts';
import { useGlobalModalActions } from '../ducks/globalModals.preload.ts';
+import { useToastActions } from '../ducks/toast.preload.ts';
export type ExternalProps = {
packId: string;
@@ -27,6 +28,7 @@ export const SmartStickerPreviewModal = memo(function SmartStickerPreviewModal({
const { downloadStickerPack, installStickerPack, uninstallStickerPack } =
useStickersActions();
const { closeStickerPackPreview } = useGlobalModalActions();
+ const { showToast } = useToastActions();
const packDb = packs[packId];
const pack = packDb
@@ -41,6 +43,7 @@ export const SmartStickerPreviewModal = memo(function SmartStickerPreviewModal({
installStickerPack={installStickerPack}
pack={pack}
uninstallStickerPack={uninstallStickerPack}
+ showToast={showToast}
/>
);
});
diff --git a/ts/test-mock/storage/sticker_test.node.ts b/ts/test-mock/storage/sticker_test.node.ts
index f8b24bace6..f16c977275 100644
--- a/ts/test-mock/storage/sticker_test.node.ts
+++ b/ts/test-mock/storage/sticker_test.node.ts
@@ -80,7 +80,7 @@ describe('stickers', function (this: Mocha.Suite) {
.click();
await window
.getByRole('dialog', { name: 'Sticker Pack' })
- .getByRole('button', { name: 'Install' })
+ .getByRole('button', { name: 'Add Stickers' })
.click();
debug('waiting for sync message');
@@ -118,12 +118,10 @@ describe('stickers', function (this: Mocha.Suite) {
debug('uninstalling first sticker pack via UI');
const state = await phone.expectStorageState('initial state');
- await conversationView
- .locator(`a:has-text("${STICKER_PACKS[0].id.toString('hex')}")`)
- .click();
+ // Dialog remains open after install
await window
.getByRole('dialog', { name: 'Sticker Pack' })
- .getByRole('button', { name: 'Uninstall' })
+ .getByRole('button', { name: 'Remove' })
.click();
// Confirm
diff --git a/ts/types/Toast.dom.tsx b/ts/types/Toast.dom.tsx
index b9602e9d66..8d7d5d93db 100644
--- a/ts/types/Toast.dom.tsx
+++ b/ts/types/Toast.dom.tsx
@@ -31,6 +31,7 @@ export enum ToastType {
ConversationUnarchived = 'ConversationUnarchived',
CopiedBackupKey = 'CopiedBackupKey',
CopiedCallLink = 'CopiedCallLink',
+ CopiedStickerPackLink = 'CopiedStickerPackLink',
CopiedUsername = 'CopiedUsername',
CopiedUsernameLink = 'CopiedUsernameLink',
DangerousFileType = 'DangerousFileType',
@@ -159,6 +160,7 @@ export type AnyToast =
| { toastType: ToastType.ConversationUnarchived }
| { toastType: ToastType.CopiedBackupKey }
| { toastType: ToastType.CopiedCallLink }
+ | { toastType: ToastType.CopiedStickerPackLink }
| { toastType: ToastType.CopiedUsername }
| { toastType: ToastType.CopiedUsernameLink }
| { toastType: ToastType.DangerousFileType }
diff --git a/ts/util/Stickers.std.ts b/ts/util/Stickers.std.ts
index 4d2bda986d..e5d6692361 100644
--- a/ts/util/Stickers.std.ts
+++ b/ts/util/Stickers.std.ts
@@ -1,6 +1,8 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
+import * as Bytes from '../Bytes.std.ts';
+
const VALID_PACK_ID_REGEXP = /^[0-9a-f]{32}$/i;
export function isPackIdValid(packId: unknown): packId is string {
@@ -10,3 +12,7 @@ export function isPackIdValid(packId: unknown): packId is string {
export function redactPackId(packId: string): string {
return `[REDACTED]${packId.slice(-3)}`;
}
+
+export function fromBase64PackKeyToHex(packKey: string): string {
+ return Bytes.fromBase64(packKey).toHex();
+}