mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-06-05 14:33:22 +01:00
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:
@@ -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:
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user