Update sticker preview modal and fix usage in sticker manager

Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2026-06-01 08:34:16 -05:00
committed by GitHub
parent 7d1b33b59e
commit 576c1b458c
15 changed files with 325 additions and 190 deletions
+24
View File
@@ -3734,6 +3734,30 @@
"messageformat": "Error opening sticker pack. Check your internet connection and try again.",
"description": "The message that appears in the sticker preview modal when there is an error."
},
"icu:stickers--StickerPreview--Install": {
"messageformat": "Add Stickers",
"description": "Primary button to install a sticker pack in the sticker pack preview modal."
},
"icu:stickers--StickerPreview--Remove": {
"messageformat": "Remove",
"description": "Button to uninstall and remove a sticker pack in the sticker pack preview modal."
},
"icu:stickers--StickerPreview--Link": {
"messageformat": "Link",
"description": "Button to copy the link URL of a sticker pack in the sticker pack preview modal."
},
"icu:stickers--StickerPreview--LinkCopied": {
"messageformat": "Link copied",
"description": "Toast text shown after copying the link URL of a sticker pack in the sticker pack preview modal."
},
"icu:stickers--StickerPreview--StickerCount": {
"messageformat": "{count, plural, one {# sticker} other {# stickers}}",
"description": "Text showing count of stickers in the sticker pack preview modal."
},
"icu:stickers--StickerPreview--StickerNoEmojiAriaLabel": {
"messageformat": "Sticker without an emoji",
"description": "Accessibility label for sticker without an assigned emoji in the sticker pack preview modal."
},
"icu:FunPicker__Tab--Emojis": {
"messageformat": "Emoji",
"description": "FunPicker > Tabs > Emojis Tab > Label (emoji plural)"
@@ -109,6 +109,8 @@ function getToast(toastType: ToastType): AnyToast {
return { toastType: ToastType.CopiedBackupKey };
case ToastType.CopiedCallLink:
return { toastType: ToastType.CopiedCallLink };
case ToastType.CopiedStickerPackLink:
return { toastType: ToastType.CopiedStickerPackLink };
case ToastType.CopiedUsername:
return { toastType: ToastType.CopiedUsername };
case ToastType.CopiedUsernameLink:
+8
View File
@@ -334,6 +334,14 @@ function renderToast({
);
}
if (toastType === ToastType.CopiedStickerPackLink) {
return (
<Toast onClose={hideToast} timeout={3 * SECOND}>
{i18n('icu:stickers--StickerPreview--LinkCopied')}
</Toast>
);
}
if (toastType === ToastType.CopiedUsername) {
return (
<Toast onClose={hideToast} timeout={3 * SECOND}>
@@ -54,6 +54,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
installedPacks: overrideProps.installedPacks || [],
knownPacks: overrideProps.knownPacks || [],
receivedPacks: overrideProps.receivedPacks || [],
showToast: action('showToast'),
uninstallStickerPack: action('uninstallStickerPack'),
});
+36 -9
View File
@@ -1,11 +1,19 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { memo, createRef, useState, useEffect, useCallback } from 'react';
import {
memo,
createRef,
useState,
useEffect,
useCallback,
useMemo,
} from 'react';
import { StickerManagerPackRow } from './StickerManagerPackRow.dom.tsx';
import { StickerPreviewModal } from './StickerPreviewModal.dom.tsx';
import type { LocalizerType } from '../../types/Util.std.ts';
import type { StickerPackType } from '../../state/ducks/stickers.preload.ts';
import type { ShowToastAction } from '../../state/ducks/toast.preload.ts';
import { Tabs } from '../Tabs.dom.tsx';
export type OwnProps = {
@@ -25,6 +33,7 @@ export type OwnProps = {
readonly installedPacks: ReadonlyArray<StickerPackType>;
readonly knownPacks?: ReadonlyArray<StickerPackType>;
readonly receivedPacks: ReadonlyArray<StickerPackType>;
readonly showToast: ShowToastAction;
readonly uninstallStickerPack: (
packId: string,
packKey: string,
@@ -48,12 +57,11 @@ export const StickerManager = memo(function StickerManagerInner({
installedPacks,
knownPacks,
receivedPacks,
showToast,
uninstallStickerPack,
}: Props) {
const focusRef = createRef<HTMLDivElement>();
const [packToPreview, setPackToPreview] = useState<StickerPackType | null>(
null
);
const [packIdToPreview, setPackIdToPreview] = useState<string | null>(null);
useEffect(() => {
if (!knownPacks) {
@@ -75,16 +83,34 @@ export const StickerManager = memo(function StickerManagerInner({
}, []);
const clearPackToPreview = useCallback(() => {
setPackToPreview(null);
}, [setPackToPreview]);
setPackIdToPreview(null);
}, [setPackIdToPreview]);
const previewPack = useCallback((pack: StickerPackType) => {
setPackToPreview(pack);
const previewPack = useCallback((packId: string) => {
setPackIdToPreview(packId);
}, []);
const allPacks = useMemo(() => {
const packsMap = new Map<string, StickerPackType>();
const packsList = [
...blessedPacks,
...installedPacks,
...(knownPacks ?? []),
...receivedPacks,
];
packsList.forEach(pack => {
packsMap.set(pack.id, pack);
});
return packsMap;
}, [blessedPacks, installedPacks, knownPacks, receivedPacks]);
const packToPreview = useMemo(() => {
return packIdToPreview ? allPacks.get(packIdToPreview) : undefined;
}, [allPacks, packIdToPreview]);
return (
<>
{packToPreview ? (
{packIdToPreview ? (
<StickerPreviewModal
closeStickerPackPreview={closeStickerPackPreview}
downloadStickerPack={downloadStickerPack}
@@ -93,6 +119,7 @@ export const StickerManager = memo(function StickerManagerInner({
onClose={clearPackToPreview}
pack={packToPreview}
uninstallStickerPack={uninstallStickerPack}
showToast={showToast}
/>
) : null}
<div
@@ -17,7 +17,7 @@ import { AxoConfirmDialog } from '../../axo/AxoConfirmDialog.dom.tsx';
export type OwnProps = {
readonly i18n: LocalizerType;
readonly pack: StickerPackType;
readonly onClickPreview?: (sticker: StickerPackType) => unknown;
readonly onClickPreview?: (packId: string) => unknown;
readonly installStickerPack?: (
packId: string,
packKey: string,
@@ -81,10 +81,10 @@ export const StickerManagerPackRow = memo(function StickerManagerPackRowInner({
event.stopPropagation();
event.preventDefault();
onClickPreview(pack);
onClickPreview(id);
}
},
[onClickPreview, pack]
[onClickPreview, id]
);
const handleClickPreview = useCallback(
@@ -93,10 +93,10 @@ export const StickerManagerPackRow = memo(function StickerManagerPackRowInner({
event.stopPropagation();
event.preventDefault();
onClickPreview(pack);
onClickPreview(id);
}
},
[onClickPreview, pack]
[onClickPreview, id]
);
return (
@@ -46,127 +46,97 @@ const tallSticker: StickerType = {
packId: 'tall',
};
export function Full(): JSX.Element {
const title = 'Foo';
const author = 'Foo McBarrington';
const pack: StickerPackType = {
id: 'foo',
key: 'foo',
lastUsed: Date.now(),
cover: abeSticker,
title: 'Foo',
isBlessed: false,
author: 'Foo McBarrington',
status: 'downloaded' as const,
stickerCount: 101,
stickers: [
wideSticker,
tallSticker,
...Array(101)
.fill(0)
.map((_n, id) => ({ ...abeSticker, id })),
],
};
const pack: StickerPackType = {
id: 'foo',
key: 'foo',
lastUsed: Date.now(),
cover: abeSticker,
title,
isBlessed: true,
author,
status: 'downloaded' as const,
stickerCount: 101,
stickers: [
wideSticker,
tallSticker,
...Array(101)
.fill(0)
.map((_n, id) => ({ ...abeSticker, id })),
],
};
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
closeStickerPackPreview: action('closeStickerPackPreview'),
downloadStickerPack: action('downloadStickerPack'),
i18n,
installStickerPack: action('installStickerPack'),
showToast: action('showToast'),
uninstallStickerPack: action('uninstallStickerPack'),
pack: overrideProps.pack ?? pack,
});
return (
<StickerPreviewModal
closeStickerPackPreview={action('closeStickerPackPreview')}
onClose={action('onClose')}
installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')}
i18n={i18n}
pack={pack}
/>
);
export function Pack(): JSX.Element {
const props = createProps();
return <StickerPreviewModal {...props} />;
}
export function JustFourStickers(): JSX.Element {
const title = 'Foo';
const author = 'Foo McBarrington';
const pack = {
id: 'foo',
key: 'foo',
lastUsed: Date.now(),
cover: abeSticker,
title,
export function OfficialPack(): JSX.Element {
const blessedPack = {
...pack,
isBlessed: true,
author,
status: 'downloaded' as const,
stickerCount: 101,
};
const props = createProps({ pack: blessedPack });
return <StickerPreviewModal {...props} />;
}
export function SmallPack(): JSX.Element {
const smallPack = {
...pack,
stickerCount: 4,
stickers: [abeSticker, abeSticker, abeSticker, abeSticker],
};
const props = createProps({ pack: smallPack });
return <StickerPreviewModal {...props} />;
}
return (
<StickerPreviewModal
closeStickerPackPreview={action('closeStickerPackPreview')}
installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')}
i18n={i18n}
pack={pack}
/>
);
export function PackInstalled(): JSX.Element {
const installedPack = {
...pack,
status: 'installed' as const,
};
const props = createProps({ pack: installedPack });
return <StickerPreviewModal {...props} />;
}
export function PackInstallPending(): JSX.Element {
const pendingPack = {
...pack,
status: 'pending' as const,
};
const props = createProps({ pack: pendingPack });
return <StickerPreviewModal {...props} />;
}
export function InitialDownload(): JSX.Element {
return (
<StickerPreviewModal
closeStickerPackPreview={action('closeStickerPackPreview')}
installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')}
i18n={i18n}
pack={{
...STICKER_PACK_DEFAULTS,
isBlessed: false,
stickers: [],
}}
/>
);
const installingPack = {
...STICKER_PACK_DEFAULTS,
isBlessed: false,
stickers: [],
};
const props = createProps({ pack: installingPack });
return <StickerPreviewModal {...props} />;
}
export function PackDeleted(): JSX.Element {
return (
<StickerPreviewModal
closeStickerPackPreview={action('closeStickerPackPreview')}
installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')}
i18n={i18n}
pack={undefined}
/>
);
const props = createProps();
return <StickerPreviewModal {...props} pack={undefined} />;
}
export function PackError(): JSX.Element {
return (
<StickerPreviewModal
closeStickerPackPreview={action('closeStickerPackPreview')}
installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')}
i18n={i18n}
pack={{
id: 'foo',
key: 'foo',
lastUsed: Date.now(),
cover: abeSticker,
title: 'Foo',
isBlessed: true,
author: 'Foo McBarrington',
status: 'error',
stickerCount: 101,
stickers: [
wideSticker,
tallSticker,
...Array(101)
.fill(0)
.map((_n, id) => ({ ...abeSticker, id })),
],
}}
/>
);
const errorPack = {
...pack,
status: 'error' as const,
};
const props = createProps({ pack: errorPack });
return <StickerPreviewModal {...props} />;
}
@@ -4,12 +4,18 @@
import { memo, useState, useEffect, useCallback } from 'react';
import type { LocalizerType } from '../../types/Util.std.ts';
import type { StickerPackType } from '../../state/ducks/stickers.preload.ts';
import type { ShowToastAction } from '../../state/ducks/toast.preload.ts';
import { UserText } from '../UserText.dom.tsx';
import { AxoConfirmDialog } from '../../axo/AxoConfirmDialog.dom.tsx';
import { AxoDialog } from '../../axo/AxoDialog.dom.tsx';
import { tw } from '../../axo/tw.dom.tsx';
import { AxoSymbol } from '../../axo/AxoSymbol.dom.tsx';
import { AxoStackedButton } from '../../axo/AxoStackedButton.dom.tsx';
import { SpinnerV2 } from '../SpinnerV2.dom.tsx';
import { OfficialChatInlineBadge } from '../conversation/OfficialChatInlineBadge.dom.tsx';
import { artAddStickersRoute } from '../../util/signalRoutes.std.ts';
import { drop } from '../../util/drop.std.ts';
import { ToastType } from '../../types/Toast.dom.tsx';
import { fromBase64PackKeyToHex } from '../../util/Stickers.std.ts';
export type Props = Readonly<{
onClose?: () => void;
@@ -29,11 +35,20 @@ export type Props = Readonly<{
packKey: string,
options: { actionSource: 'ui' }
) => void;
showToast: ShowToastAction;
pack?: StickerPackType;
i18n: LocalizerType;
}>;
function renderBody({ pack, i18n }: Pick<Props, 'i18n' | 'pack'>) {
function renderBody({
pack,
i18n,
handleCopyLink,
handleStartUninstall,
}: Pick<Props, 'i18n' | 'pack'> & {
handleCopyLink: () => void;
handleStartUninstall: () => void;
}) {
if (pack == null) {
return null;
}
@@ -64,29 +79,94 @@ function renderBody({ pack, i18n }: Pick<Props, 'i18n' | 'pack'>) {
const placeholders = pack.stickerCount - pack.stickers.length;
return (
<div className={tw('grid grid-cols-4 items-center justify-center gap-2')}>
{pack.stickers.map(({ id, url }) => (
<div className={tw('justify-items-center')}>
{pack.cover && (
<img
key={id}
className={tw(
'aspect-square max-h-24 w-full max-w-24 object-contain'
'mb-4 aspect-square max-h-20 w-full max-w-20 object-contain'
)}
src={url}
src={pack.cover.url}
alt={pack.title}
/>
))}
{Array.from({ length: placeholders }, (_, index) => {
return (
<div
key={index}
className={tw('aspect-square rounded-md bg-fill-secondary')}
)}
<h2 className={tw('mb-2 type-title-medium')}>
<UserText text={pack.title} />
{pack.isBlessed && (
<span className={tw('ms-1.5')}>
<OfficialChatInlineBadge />
</span>
)}
</h2>
<div
className={tw(
'mb-3 justify-items-center type-body-medium text-label-secondary'
)}
>
<div>{pack.author}</div>
<div>
{i18n('icu:stickers--StickerPreview--StickerCount', {
count: pack.stickerCount,
})}
</div>
</div>
<AxoStackedButton.Row spacing="md">
<AxoStackedButton.Root
symbol="link"
label={i18n('icu:stickers--StickerPreview--Link')}
onClick={handleCopyLink}
/>
{pack.status === 'installed' && (
<AxoStackedButton.Root
symbol="minus-circle"
label={i18n('icu:stickers--StickerPreview--Remove')}
onClick={handleStartUninstall}
/>
);
})}
)}
</AxoStackedButton.Row>
<div
className={tw(
'mt-4 grid w-max grid-cols-5 items-center justify-center gap-2.5'
)}
>
{pack.stickers.map(({ emoji, id, url }) => (
<img
key={id}
className={tw(
'aspect-square max-h-18 w-full max-w-18 object-contain'
)}
src={url}
alt={
emoji ??
i18n('icu:stickers--StickerPreview--StickerNoEmojiAriaLabel')
}
/>
))}
{Array.from({ length: placeholders }, (_, index) => {
return (
<div
key={index}
className={tw(
'aspect-square max-h-18 w-full max-w-18 rounded-md bg-fill-secondary'
)}
/>
);
})}
</div>
</div>
);
}
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 (
<>
<AxoDialog.Root open onOpenChange={handleClose}>
<AxoDialog.Content size="md" escape="cancel-is-noop">
<AxoDialog.Header>
<AxoDialog.Title>
<AxoDialog.Title screenReaderOnly>
{i18n('icu:stickers--StickerPreview--Title')}
</AxoDialog.Title>
<AxoDialog.Close />
</AxoDialog.Header>
<AxoDialog.Body>{renderBody({ pack, i18n })}</AxoDialog.Body>
<AxoDialog.Body>
{renderBody({ pack, i18n, handleCopyLink, handleStartUninstall })}
</AxoDialog.Body>
<AxoDialog.Footer>
{pack != null && pack.status != null && pack.status !== 'error' && (
<AxoDialog.FooterContent>
<h3 className={tw('text-label-primary')}>
<UserText text={pack.title} />
{pack.isBlessed && (
<span className={tw('text-color-fill-primary')}>
{' '}
<AxoSymbol.InlineGlyph
symbol="check-circle-fill"
label={null}
/>
</span>
)}
</h3>
<p className={tw('text-label-secondary')}>{pack.author}</p>
</AxoDialog.FooterContent>
{isInstallFooterVisible(pack) && (
<AxoDialog.Action
variant="primary"
onClick={handleInstall}
pending={pack.status === 'pending'}
>
{i18n('icu:stickers--StickerPreview--Install')}
</AxoDialog.Action>
)}
<AxoDialog.Actions>
{isInstalled ? (
<AxoDialog.Action
variant="destructive"
onClick={handleStartUninstall}
>
{i18n('icu:stickers--StickerManager--Uninstall')}
</AxoDialog.Action>
) : (
<AxoDialog.Action
variant="primary"
onClick={handleInstall}
pending={pack?.status === 'pending'}
>
{i18n('icu:stickers--StickerManager--Install')}
</AxoDialog.Action>
)}
</AxoDialog.Actions>
</AxoDialog.Footer>
</AxoDialog.Content>
</AxoDialog.Root>
+26 -8
View File
@@ -397,14 +397,32 @@ export function reducer(
action: Readonly<StickersActionType | EraseStorageServiceStateAction>
): 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,
+6 -1
View File
@@ -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 <SmartStickerManager />;
return (
<ErrorBoundary name="StickerManager">
<SmartStickerManager />
</ErrorBoundary>
);
}
log.warn(toLogFormat(missingCaseError(panel.type)));
@@ -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 (
<StickerManager
@@ -36,6 +38,7 @@ export const SmartStickerManager = memo(function SmartStickerManager() {
knownPacks={knownPacks}
receivedPacks={receivedPacks}
uninstallStickerPack={uninstallStickerPack}
showToast={showToast}
/>
);
});
@@ -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}
/>
);
});
+3 -5
View File
@@ -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
+2
View File
@@ -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 }
+6
View File
@@ -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();
}