Refresh StickerManager to use design system components
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
parent
108241e477
commit
12c7df49f3
@ -3698,6 +3698,14 @@
|
||||
"messageformat": "No stickers installed",
|
||||
"description": "Shown in the sticker pack manager when you don't have any installed sticker packs."
|
||||
},
|
||||
"icu:stickers--StickerManager--MyStickers--None": {
|
||||
"messageformat": "No sticker packs, add stickers to send to your friends.",
|
||||
"description": "Text shown in the sticker pack manager on the installed stickers tab when you don't have any installed sticker packs."
|
||||
},
|
||||
"icu:stickers--StickerManager--AddStickers": {
|
||||
"messageformat": "Add stickers",
|
||||
"description": "Button in the sticker pack manager on the installed stickers tab when you don't have any installed sticker packs. The button takes you to the All Stickers tab where you can install stickers."
|
||||
},
|
||||
"icu:stickers--StickerManager--BlessedPacks": {
|
||||
"messageformat": "Signal Artist Series",
|
||||
"description": "Shown in the sticker pack manager above the default sticker packs."
|
||||
@ -3710,6 +3718,14 @@
|
||||
"messageformat": "Stickers You Received",
|
||||
"description": "Shown in the sticker pack manager above sticker packs which you have received in messages."
|
||||
},
|
||||
"icu:stickers--StickerManager--ReceivedPacks2": {
|
||||
"messageformat": "Shared With You",
|
||||
"description": "Shown in the sticker pack manager above sticker packs which you have received in messages."
|
||||
},
|
||||
"icu:stickers--StickerManager--ReceivedPacksDescription": {
|
||||
"messageformat": "When you receive a sticker from someone, the sticker pack will appear here.",
|
||||
"description": "Section description shown in the sticker pack manager above sticker packs which you have received in messages."
|
||||
},
|
||||
"icu:stickers--StickerManager--ReceivedPacks--Empty": {
|
||||
"messageformat": "Stickers from incoming messages will appear here",
|
||||
"description": "Shown in the sticker pack manager when you have not received any sticker packs in messages."
|
||||
@ -3718,6 +3734,10 @@
|
||||
"messageformat": "Install",
|
||||
"description": "Shown in the sticker pack manager next to sticker packs which can be installed."
|
||||
},
|
||||
"icu:stickers--StickerManager--Installed": {
|
||||
"messageformat": "Installed",
|
||||
"description": "Accessibility label in the sticker pack manager next to sticker packs which are already installed."
|
||||
},
|
||||
"icu:stickers--StickerManager--Uninstall": {
|
||||
"messageformat": "Uninstall",
|
||||
"description": "Shown in the sticker pack manager next to sticker packs which are already installed."
|
||||
@ -3726,6 +3746,22 @@
|
||||
"messageformat": "You may not be able to re-install this sticker pack if you no longer have the source message.",
|
||||
"description": "Shown in the sticker pack manager next to sticker packs which are already installed."
|
||||
},
|
||||
"icu:stickers--StickerManagerHeader--All": {
|
||||
"messageformat": "All",
|
||||
"description": "Shown in the sticker pack manager in the navigation tabs. This tab shows all sticker packs available to install."
|
||||
},
|
||||
"icu:stickers--StickerManagerHeader--MyStickers": {
|
||||
"messageformat": "My Stickers",
|
||||
"description": "Shown in the sticker pack manager in the navigation tabs. This tab shows all sticker packs you have currently installed."
|
||||
},
|
||||
"icu:stickers--StickerManagerPackContextMenu--Add": {
|
||||
"messageformat": "Add",
|
||||
"description": "Context menu item to add a sticker pack. Shown in the sticker pack manager when right clicking a sticker pack."
|
||||
},
|
||||
"icu:stickers--StickerManagerPackContextMenu--Remove": {
|
||||
"messageformat": "Remove",
|
||||
"description": "Context menu item to remove a sticker pack. Shown in the sticker pack manager when right clicking a sticker pack."
|
||||
},
|
||||
"icu:stickers--StickerPreview--Title": {
|
||||
"messageformat": "Sticker Pack",
|
||||
"description": "The title that appears in the sticker pack preview modal."
|
||||
|
||||
@ -10,95 +10,13 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.module-sticker-manager__text {
|
||||
height: 18px;
|
||||
|
||||
letter-spacing: 0px;
|
||||
line-height: 18px;
|
||||
padding-inline-start: 8px;
|
||||
|
||||
@include mixins.light-theme() {
|
||||
color: variables.$color-gray-60;
|
||||
}
|
||||
|
||||
@include mixins.dark-theme() {
|
||||
color: variables.$color-gray-25;
|
||||
}
|
||||
|
||||
&--heading {
|
||||
@include mixins.font-body-1-bold;
|
||||
|
||||
@include mixins.light-theme() {
|
||||
color: variables.$color-gray-90;
|
||||
}
|
||||
|
||||
@include mixins.dark-theme() {
|
||||
color: variables.$color-gray-05;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-sticker-manager__empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
|
||||
@include mixins.light-theme {
|
||||
background: variables.$color-gray-02;
|
||||
color: variables.$color-gray-60;
|
||||
}
|
||||
|
||||
@include mixins.dark-theme {
|
||||
background: variables.$color-gray-90;
|
||||
color: variables.$color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
%blessed-sticker-pack-icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-radius: 8px;
|
||||
background-color: variables.$color-white;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-inline-start: 5px;
|
||||
margin-bottom: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
inset-inline-start: -1px;
|
||||
|
||||
@include mixins.light-theme {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/check/check-circle-fill.svg',
|
||||
variables.$color-accent-blue
|
||||
);
|
||||
}
|
||||
|
||||
@include mixins.dark-theme {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/check/check-circle-fill.svg',
|
||||
variables.$color-accent-blue
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-sticker-manager__pack-row {
|
||||
@include mixins.button-reset;
|
||||
|
||||
& {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 16px;
|
||||
padding: 10px;
|
||||
padding-inline-start: 8px;
|
||||
}
|
||||
|
||||
@ -109,13 +27,13 @@
|
||||
}
|
||||
|
||||
&__cover {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
}
|
||||
&__cover-placeholder {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: variables.$color-gray-05;
|
||||
}
|
||||
|
||||
@ -128,26 +46,6 @@
|
||||
padding-block: 0;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__author {
|
||||
flex: 1;
|
||||
|
||||
@include mixins.light-theme() {
|
||||
color: variables.$color-gray-45;
|
||||
}
|
||||
|
||||
@include mixins.dark-theme() {
|
||||
color: variables.$color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
&__blessed-icon {
|
||||
@extend %blessed-sticker-pack-icon;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
@ -182,40 +80,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-sticker-manager__install-button {
|
||||
background: none;
|
||||
border: 0;
|
||||
color: variables.$color-gray-90;
|
||||
|
||||
@include mixins.font-body-1-bold;
|
||||
|
||||
height: 24px;
|
||||
background: variables.$color-gray-05;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-block: 0;
|
||||
padding-inline: 12px;
|
||||
|
||||
@include mixins.dark-theme {
|
||||
color: variables.$color-gray-05;
|
||||
background: variables.$color-gray-75;
|
||||
}
|
||||
|
||||
@include mixins.mouse-mode {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&--blue {
|
||||
@include mixins.light-theme {
|
||||
background: variables.$color-ultramarine;
|
||||
color: variables.$color-white;
|
||||
}
|
||||
@include mixins.dark-theme {
|
||||
background: variables.$color-ultramarine-light;
|
||||
color: variables.$color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,11 +12,18 @@ import {
|
||||
sticker1,
|
||||
sticker2,
|
||||
} from '../../test-helpers/stickersMocks.std.ts';
|
||||
import type { StickerPackType } from '../../state/ducks/stickers.preload.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'Components/Stickers/StickerManager',
|
||||
argTypes: {
|
||||
tab: {
|
||||
options: ['all', 'my-stickers'],
|
||||
control: { type: 'select' },
|
||||
},
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
const receivedPacks = [
|
||||
@ -31,11 +38,21 @@ const installedPacks = [
|
||||
|
||||
const blessedPacks = [
|
||||
createPack(
|
||||
{ id: 'blessed-pack-1', status: 'downloaded', isBlessed: true },
|
||||
{
|
||||
id: 'blessed-pack-1',
|
||||
status: 'downloaded',
|
||||
isBlessed: true,
|
||||
author: 'Ann Chovy',
|
||||
},
|
||||
sticker1
|
||||
),
|
||||
createPack(
|
||||
{ id: 'blessed-pack-2', status: 'downloaded', isBlessed: true },
|
||||
{
|
||||
id: 'blessed-pack-2',
|
||||
status: 'downloaded',
|
||||
isBlessed: true,
|
||||
author: 'Tom Ato',
|
||||
},
|
||||
sticker2
|
||||
),
|
||||
];
|
||||
@ -54,36 +71,51 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
installedPacks: overrideProps.installedPacks || [],
|
||||
knownPacks: overrideProps.knownPacks || [],
|
||||
receivedPacks: overrideProps.receivedPacks || [],
|
||||
tab: overrideProps.tab || 'all',
|
||||
setTab: action('setTab'),
|
||||
showToast: action('showToast'),
|
||||
uninstallStickerPack: action('uninstallStickerPack'),
|
||||
});
|
||||
|
||||
export function Full(): JSX.Element {
|
||||
const props = createProps({ installedPacks, receivedPacks, blessedPacks });
|
||||
export function Full(args: Props): JSX.Element {
|
||||
const props = createProps({ blessedPacks, installedPacks, receivedPacks });
|
||||
|
||||
return <StickerManager {...props} />;
|
||||
return <StickerManager {...props} {...args} />;
|
||||
}
|
||||
|
||||
export function InstalledPacks(): JSX.Element {
|
||||
const props = createProps({ installedPacks });
|
||||
export function ReceivedPacks(args: Props): JSX.Element {
|
||||
const props = createProps({ blessedPacks, receivedPacks });
|
||||
|
||||
return <StickerManager {...props} />;
|
||||
return <StickerManager {...props} {...args} />;
|
||||
}
|
||||
|
||||
export function ReceivedPacks(): JSX.Element {
|
||||
const props = createProps({ receivedPacks });
|
||||
export function InstalledPacks(args: Props): JSX.Element {
|
||||
const blessedPacksWithInstalled = [
|
||||
{ ...blessedPacks[0], status: 'installed' },
|
||||
blessedPacks[1],
|
||||
] as Array<StickerPackType>;
|
||||
const props = createProps({
|
||||
blessedPacks: blessedPacksWithInstalled,
|
||||
installedPacks,
|
||||
receivedPacks: installedPacks,
|
||||
});
|
||||
|
||||
return <StickerManager {...props} />;
|
||||
return <StickerManager {...props} {...args} />;
|
||||
}
|
||||
|
||||
export function InstalledAndKnownPacks(): JSX.Element {
|
||||
const props = createProps({ installedPacks, knownPacks });
|
||||
export function InstalledAndKnownPacks(args: Props): JSX.Element {
|
||||
const props = createProps({
|
||||
blessedPacks,
|
||||
knownPacks,
|
||||
installedPacks,
|
||||
receivedPacks: installedPacks,
|
||||
});
|
||||
|
||||
return <StickerManager {...props} />;
|
||||
return <StickerManager {...props} {...args} />;
|
||||
}
|
||||
|
||||
export function Empty(): JSX.Element {
|
||||
export function Empty(args: Props): JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return <StickerManager {...props} />;
|
||||
return <StickerManager {...props} {...args} />;
|
||||
}
|
||||
|
||||
@ -8,13 +8,16 @@ import {
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
type JSX,
|
||||
} 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';
|
||||
import type { StickerManagerTabType } from '../../types/Stickers.preload.ts';
|
||||
import { tw } from '../../axo/tw.dom.tsx';
|
||||
import { AxoButton } from '../../axo/AxoButton.dom.tsx';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly blessedPacks: ReadonlyArray<StickerPackType>;
|
||||
@ -33,6 +36,8 @@ export type OwnProps = {
|
||||
readonly installedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly knownPacks?: ReadonlyArray<StickerPackType>;
|
||||
readonly receivedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly tab: StickerManagerTabType;
|
||||
readonly setTab: (value: StickerManagerTabType) => void;
|
||||
readonly showToast: ShowToastAction;
|
||||
readonly uninstallStickerPack: (
|
||||
packId: string,
|
||||
@ -43,11 +48,6 @@ export type OwnProps = {
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
||||
enum TabViews {
|
||||
Available = 'Available',
|
||||
Installed = 'Installed',
|
||||
}
|
||||
|
||||
export const StickerManager = memo(function StickerManagerInner({
|
||||
blessedPacks,
|
||||
closeStickerPackPreview,
|
||||
@ -57,6 +57,8 @@ export const StickerManager = memo(function StickerManagerInner({
|
||||
installedPacks,
|
||||
knownPacks,
|
||||
receivedPacks,
|
||||
tab,
|
||||
setTab,
|
||||
showToast,
|
||||
uninstallStickerPack,
|
||||
}: Props) {
|
||||
@ -90,6 +92,22 @@ export const StickerManager = memo(function StickerManagerInner({
|
||||
setPackIdToPreview(packId);
|
||||
}, []);
|
||||
|
||||
const renderStickerPackRow = useCallback(
|
||||
(pack: StickerPackType): JSX.Element => (
|
||||
<StickerManagerPackRow
|
||||
key={pack.id}
|
||||
pack={pack}
|
||||
i18n={i18n}
|
||||
onClickPreview={previewPack}
|
||||
installStickerPack={installStickerPack}
|
||||
uninstallStickerPack={uninstallStickerPack}
|
||||
/>
|
||||
),
|
||||
[i18n, installStickerPack, previewPack, uninstallStickerPack]
|
||||
);
|
||||
|
||||
const setTabAll = useCallback(() => setTab('all'), [setTab]);
|
||||
|
||||
const allPacks = useMemo(() => {
|
||||
const packsMap = new Map<string, StickerPackType>();
|
||||
const packsList = [
|
||||
@ -99,6 +117,10 @@ export const StickerManager = memo(function StickerManagerInner({
|
||||
...receivedPacks,
|
||||
];
|
||||
packsList.forEach(pack => {
|
||||
if (packsMap.get(pack.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
packsMap.set(pack.id, pack);
|
||||
});
|
||||
return packsMap;
|
||||
@ -123,95 +145,83 @@ export const StickerManager = memo(function StickerManagerInner({
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className="module-sticker-manager"
|
||||
className={tw('m-auto max-w-152 px-4 py-0 outline-none')}
|
||||
data-testid="StickerManager"
|
||||
tabIndex={-1}
|
||||
ref={focusRef}
|
||||
>
|
||||
<Tabs
|
||||
initialSelectedTab={TabViews.Available}
|
||||
tabs={[
|
||||
{
|
||||
id: TabViews.Available,
|
||||
label: i18n('icu:stickers--StickerManager--Available'),
|
||||
},
|
||||
{
|
||||
id: TabViews.Installed,
|
||||
label: i18n('icu:stickers--StickerManager--InstalledPacks'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{({ selectedTab }) => (
|
||||
<>
|
||||
{selectedTab === TabViews.Available && (
|
||||
<>
|
||||
<h2 className="module-sticker-manager__text module-sticker-manager__text--heading">
|
||||
{i18n('icu:stickers--StickerManager--BlessedPacks')}
|
||||
</h2>
|
||||
{blessedPacks.length > 0 ? (
|
||||
blessedPacks.map(pack => (
|
||||
<StickerManagerPackRow
|
||||
key={pack.id}
|
||||
pack={pack}
|
||||
i18n={i18n}
|
||||
onClickPreview={previewPack}
|
||||
installStickerPack={installStickerPack}
|
||||
uninstallStickerPack={uninstallStickerPack}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="module-sticker-manager__empty">
|
||||
{i18n(
|
||||
'icu:stickers--StickerManager--BlessedPacks--Empty'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2 className="module-sticker-manager__text module-sticker-manager__text--heading">
|
||||
{i18n('icu:stickers--StickerManager--ReceivedPacks')}
|
||||
</h2>
|
||||
{receivedPacks.length > 0 ? (
|
||||
receivedPacks.map(pack => (
|
||||
<StickerManagerPackRow
|
||||
key={pack.id}
|
||||
pack={pack}
|
||||
i18n={i18n}
|
||||
onClickPreview={previewPack}
|
||||
installStickerPack={installStickerPack}
|
||||
uninstallStickerPack={uninstallStickerPack}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="module-sticker-manager__empty">
|
||||
{i18n(
|
||||
'icu:stickers--StickerManager--ReceivedPacks--Empty'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{tab === 'all' && (
|
||||
<>
|
||||
<h2
|
||||
className={tw(
|
||||
'mx-2 my-1 type-body-medium font-semibold text-label-primary select-none'
|
||||
)}
|
||||
{selectedTab === TabViews.Installed &&
|
||||
(installedPacks.length > 0 ? (
|
||||
installedPacks.map(pack => (
|
||||
<StickerManagerPackRow
|
||||
key={pack.id}
|
||||
pack={pack}
|
||||
i18n={i18n}
|
||||
onClickPreview={previewPack}
|
||||
installStickerPack={installStickerPack}
|
||||
uninstallStickerPack={uninstallStickerPack}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="module-sticker-manager__empty">
|
||||
{i18n(
|
||||
'icu:stickers--StickerManager--InstalledPacks--Empty'
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Tabs>
|
||||
>
|
||||
{i18n('icu:stickers--StickerManager--BlessedPacks')}
|
||||
</h2>
|
||||
{blessedPacks.length > 0 ? (
|
||||
blessedPacks.map(pack => renderStickerPackRow(pack))
|
||||
) : (
|
||||
<p
|
||||
className={tw(
|
||||
'mx-2 mb-1 type-body-small text-label-secondary select-none'
|
||||
)}
|
||||
>
|
||||
{i18n('icu:stickers--StickerManager--BlessedPacks--Empty')}
|
||||
</p>
|
||||
)}
|
||||
<div className={tw('mb-4')} />
|
||||
|
||||
{receivedPacks.length > 0 && (
|
||||
<>
|
||||
<h2
|
||||
className={tw(
|
||||
'mx-2 mt-2 mb-0.5 type-body-medium font-semibold text-label-primary select-none'
|
||||
)}
|
||||
>
|
||||
{i18n('icu:stickers--StickerManager--ReceivedPacks2')}
|
||||
</h2>
|
||||
<p
|
||||
className={tw(
|
||||
'mx-2 mb-1 type-body-small text-label-secondary select-none'
|
||||
)}
|
||||
>
|
||||
{i18n(
|
||||
'icu:stickers--StickerManager--ReceivedPacksDescription'
|
||||
)}
|
||||
</p>
|
||||
{receivedPacks.map(pack => renderStickerPackRow(pack))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{tab === 'my-stickers' &&
|
||||
(installedPacks.length > 0 ? (
|
||||
installedPacks.map(pack => renderStickerPackRow(pack))
|
||||
) : (
|
||||
<div
|
||||
className={tw(
|
||||
'm-auto grid min-h-screen place-items-center text-center'
|
||||
)}
|
||||
>
|
||||
<div className={tw('max-w-60')}>
|
||||
<div
|
||||
className={tw('mb-4 type-body-medium text-label-secondary')}
|
||||
>
|
||||
{i18n('icu:stickers--StickerManager--MyStickers--None')}
|
||||
</div>
|
||||
<div>
|
||||
<AxoButton.Root
|
||||
variant="secondary"
|
||||
size="md"
|
||||
onClick={setTabAll}
|
||||
>
|
||||
{i18n('icu:stickers--StickerManager--AddStickers')}
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
100
ts/components/stickers/StickerManagerHeader.dom.tsx
Normal file
100
ts/components/stickers/StickerManagerHeader.dom.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useCallback, type JSX } from 'react';
|
||||
|
||||
import { tw } from '../../axo/tw.dom.tsx';
|
||||
import { ExperimentalAxoSegmentedControl } from '../../axo/AxoSegmentedControl.dom.tsx';
|
||||
import { AxoSelect } from '../../axo/AxoSelect.dom.tsx';
|
||||
import type { LocalizerType } from '../../types/Util.std.ts';
|
||||
import type { StickerManagerTabType } from '../../types/Stickers.preload.ts';
|
||||
|
||||
// Provided by smart layer
|
||||
export type Props = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
tab: StickerManagerTabType;
|
||||
setTab: (newTab: StickerManagerTabType) => void;
|
||||
}>;
|
||||
|
||||
const TabValue = {
|
||||
All: 'all',
|
||||
MyStickers: 'my-stickers',
|
||||
} as const satisfies Record<string, StickerManagerTabType>;
|
||||
|
||||
export function StickerManagerHeader({
|
||||
i18n,
|
||||
tab,
|
||||
setTab,
|
||||
}: Props): JSX.Element {
|
||||
const setSelectedTabWithDefault = useCallback(
|
||||
(value: string | null) => {
|
||||
switch (value) {
|
||||
case 'my-stickers':
|
||||
setTab(value);
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
default:
|
||||
setTab('all');
|
||||
break;
|
||||
}
|
||||
},
|
||||
[setTab]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tw('@container', 'grow', 'flex flex-row justify-center-safe')}
|
||||
>
|
||||
<div className={tw('grow')} />
|
||||
|
||||
<div className={tw('hidden max-w-60 grow @min-[200px]:block')}>
|
||||
<ExperimentalAxoSegmentedControl.Root
|
||||
variant="track"
|
||||
width="full"
|
||||
itemWidth="equal"
|
||||
value={tab}
|
||||
onValueChange={setSelectedTabWithDefault}
|
||||
>
|
||||
<ExperimentalAxoSegmentedControl.Item value={TabValue.All}>
|
||||
<ExperimentalAxoSegmentedControl.ItemText>
|
||||
{i18n('icu:stickers--StickerManagerHeader--All')}
|
||||
</ExperimentalAxoSegmentedControl.ItemText>
|
||||
</ExperimentalAxoSegmentedControl.Item>
|
||||
<ExperimentalAxoSegmentedControl.Item value={TabValue.MyStickers}>
|
||||
<ExperimentalAxoSegmentedControl.ItemText>
|
||||
{i18n('icu:stickers--StickerManagerHeader--MyStickers')}
|
||||
</ExperimentalAxoSegmentedControl.ItemText>
|
||||
</ExperimentalAxoSegmentedControl.Item>
|
||||
</ExperimentalAxoSegmentedControl.Root>
|
||||
</div>
|
||||
|
||||
<div className={tw('block @min-[200px]:hidden')}>
|
||||
<AxoSelect.Root value={tab} onValueChange={setSelectedTabWithDefault}>
|
||||
<AxoSelect.Trigger
|
||||
variant="floating"
|
||||
width="fit"
|
||||
placeholder=""
|
||||
chevron="always"
|
||||
/>
|
||||
<AxoSelect.Content position="dropdown">
|
||||
<AxoSelect.Item value={TabValue.All}>
|
||||
<AxoSelect.ItemText>
|
||||
{i18n('icu:stickers--StickerManagerHeader--All')}
|
||||
</AxoSelect.ItemText>
|
||||
</AxoSelect.Item>
|
||||
<AxoSelect.Item value={TabValue.MyStickers}>
|
||||
<AxoSelect.ItemText>
|
||||
{i18n('icu:stickers--StickerManagerHeader--MyStickers')}
|
||||
</AxoSelect.ItemText>
|
||||
</AxoSelect.Item>
|
||||
</AxoSelect.Content>
|
||||
</AxoSelect.Root>
|
||||
</div>
|
||||
|
||||
<div className={tw('grow')} />
|
||||
|
||||
<div className={tw('min-w-4.5')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -7,12 +7,16 @@ import {
|
||||
useCallback,
|
||||
type MouseEvent,
|
||||
type KeyboardEvent,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import type { LocalizerType } from '../../types/Util.std.ts';
|
||||
import type { StickerPackType } from '../../state/ducks/stickers.preload.ts';
|
||||
import { Button, ButtonVariant } from '../Button.dom.tsx';
|
||||
import { UserText } from '../UserText.dom.tsx';
|
||||
import { AxoConfirmDialog } from '../../axo/AxoConfirmDialog.dom.tsx';
|
||||
import { AxoIconButton } from '../../axo/AxoIconButton.dom.tsx';
|
||||
import { tw } from '../../axo/tw.dom.tsx';
|
||||
import { OfficialChatInlineBadge } from '../conversation/OfficialChatInlineBadge.dom.tsx';
|
||||
import { AxoContextMenu } from '../../axo/AxoContextMenu.dom.tsx';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
@ -47,7 +51,7 @@ export const StickerManagerPackRow = memo(function StickerManagerPackRowInner({
|
||||
}, [setUninstalling]);
|
||||
|
||||
const handleInstall = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
(e: Event | MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (installStickerPack) {
|
||||
installStickerPack(id, key, { actionSource: 'ui' });
|
||||
@ -57,7 +61,7 @@ export const StickerManagerPackRow = memo(function StickerManagerPackRowInner({
|
||||
);
|
||||
|
||||
const handleUninstall = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
(e: Event) => {
|
||||
e.stopPropagation();
|
||||
if (isBlessed && uninstallStickerPack) {
|
||||
uninstallStickerPack(id, key, { actionSource: 'ui' });
|
||||
@ -116,55 +120,98 @@ export const StickerManagerPackRow = memo(function StickerManagerPackRowInner({
|
||||
{i18n('icu:stickers--StickerManager--Uninstall')}
|
||||
</AxoConfirmDialog.Action>
|
||||
</AxoConfirmDialog.Root>
|
||||
<div
|
||||
tabIndex={0}
|
||||
// This can't be a button because we have buttons as descendants
|
||||
role="button"
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleClickPreview}
|
||||
className="module-sticker-manager__pack-row"
|
||||
data-testid={id}
|
||||
<PackContextMenu
|
||||
i18n={i18n}
|
||||
handleInstall={handleInstall}
|
||||
handleUninstall={handleUninstall}
|
||||
isInstalled={pack.status === 'installed'}
|
||||
>
|
||||
{pack.cover ? (
|
||||
<img
|
||||
src={pack.cover.url}
|
||||
alt={pack.title}
|
||||
className="module-sticker-manager__pack-row__cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="module-sticker-manager__pack-row__cover-placeholder" />
|
||||
)}
|
||||
<div className="module-sticker-manager__pack-row__meta">
|
||||
<div className="module-sticker-manager__pack-row__meta__title">
|
||||
<UserText text={pack.title} />
|
||||
{pack.isBlessed ? (
|
||||
<span className="module-sticker-manager__pack-row__meta__blessed-icon" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="module-sticker-manager__pack-row__meta__author">
|
||||
{pack.author}
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-sticker-manager__pack-row__controls">
|
||||
{pack.status === 'installed' ? (
|
||||
<Button
|
||||
aria-label={i18n('icu:stickers--StickerManager--Uninstall')}
|
||||
variant={ButtonVariant.Secondary}
|
||||
onClick={handleUninstall}
|
||||
>
|
||||
{i18n('icu:stickers--StickerManager--Uninstall')}
|
||||
</Button>
|
||||
<div
|
||||
tabIndex={0}
|
||||
// This can't be a button because we have buttons as descendants
|
||||
role="button"
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleClickPreview}
|
||||
className="module-sticker-manager__pack-row"
|
||||
data-testid={id}
|
||||
>
|
||||
{pack.cover ? (
|
||||
<img
|
||||
src={pack.cover.url}
|
||||
alt={pack.title}
|
||||
className="module-sticker-manager__pack-row__cover"
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={i18n('icu:stickers--StickerManager--Install')}
|
||||
variant={ButtonVariant.Secondary}
|
||||
onClick={handleInstall}
|
||||
>
|
||||
{i18n('icu:stickers--StickerManager--Install')}
|
||||
</Button>
|
||||
<div className="module-sticker-manager__pack-row__cover-placeholder" />
|
||||
)}
|
||||
<div className="module-sticker-manager__pack-row__meta">
|
||||
<div
|
||||
className={tw(
|
||||
'mb-0.5 flex flex-1 type-body-medium text-label-primary'
|
||||
)}
|
||||
>
|
||||
<UserText text={pack.title} />
|
||||
{pack.isBlessed ? (
|
||||
<span className={tw('ms-1')}>
|
||||
<OfficialChatInlineBadge />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className={tw('flex flex-1 type-body-small text-label-secondary')}
|
||||
>
|
||||
{pack.author}
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-sticker-manager__pack-row__controls">
|
||||
{pack.status === 'installed' ? (
|
||||
<AxoIconButton.Root
|
||||
variant="secondary"
|
||||
size="md"
|
||||
symbol="check"
|
||||
label={i18n('icu:stickers--StickerManager--Installed')}
|
||||
tooltip={false}
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<AxoIconButton.Root
|
||||
variant="secondary"
|
||||
size="md"
|
||||
symbol="plus"
|
||||
label={i18n('icu:stickers--StickerManager--Install')}
|
||||
onClick={handleInstall}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PackContextMenu>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function PackContextMenu(props: {
|
||||
i18n: LocalizerType;
|
||||
isInstalled: boolean;
|
||||
handleInstall: (e: Event) => void;
|
||||
handleUninstall: (e: Event) => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { i18n, isInstalled, handleInstall, handleUninstall } = props;
|
||||
|
||||
return (
|
||||
<AxoContextMenu.Root>
|
||||
<AxoContextMenu.Trigger>{props.children}</AxoContextMenu.Trigger>
|
||||
<AxoContextMenu.Content>
|
||||
{isInstalled ? (
|
||||
<AxoContextMenu.Item symbol="minus-circle" onSelect={handleUninstall}>
|
||||
{i18n('icu:stickers--StickerManagerPackContextMenu--Remove')}
|
||||
</AxoContextMenu.Item>
|
||||
) : (
|
||||
<AxoContextMenu.Item symbol="plus-circle" onSelect={handleInstall}>
|
||||
{i18n('icu:stickers--StickerManagerPackContextMenu--Add')}
|
||||
</AxoContextMenu.Item>
|
||||
)}
|
||||
</AxoContextMenu.Content>
|
||||
</AxoContextMenu.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import { DataReader, DataWriter } from '../../sql/Client.preload.ts';
|
||||
import type {
|
||||
ActionSourceType,
|
||||
RecentStickerType,
|
||||
StickerManagerTabType,
|
||||
} from '../../types/Stickers.preload.ts';
|
||||
import {
|
||||
downloadStickerPack as externalDownloadStickerPack,
|
||||
@ -42,6 +43,7 @@ export type StickersStateType = ReadonlyDeep<{
|
||||
packs: Dictionary<StickerPackDBType>;
|
||||
recentStickers: Array<RecentStickerType>;
|
||||
blessedPacks: Dictionary<boolean>;
|
||||
stickerManagerTab: StickerManagerTabType;
|
||||
}>;
|
||||
|
||||
// These are for the React components
|
||||
@ -138,10 +140,16 @@ type UseStickerFulfilledAction = ReadonlyDeep<{
|
||||
payload: UseStickerPayloadType;
|
||||
}>;
|
||||
|
||||
type SetStickerManagerTabAction = ReadonlyDeep<{
|
||||
type: 'stickers/SET_STICKER_MANAGER_TAB';
|
||||
payload: StickerManagerTabType;
|
||||
}>;
|
||||
|
||||
export type StickersActionType = ReadonlyDeep<
|
||||
| ClearInstalledStickerPackAction
|
||||
| InstallStickerPackFulfilledAction
|
||||
| NoopActionType
|
||||
| SetStickerManagerTabAction
|
||||
| StickerAddedAction
|
||||
| StickerPackAddedAction
|
||||
| StickerPackRemovedAction
|
||||
@ -157,6 +165,7 @@ export const actions = {
|
||||
downloadStickerPack,
|
||||
installStickerPack,
|
||||
removeStickerPack,
|
||||
setStickerManagerTab,
|
||||
stickerAdded,
|
||||
stickerPackAdded,
|
||||
stickerPackUpdated,
|
||||
@ -381,6 +390,15 @@ async function doUseSticker(
|
||||
};
|
||||
}
|
||||
|
||||
function setStickerManagerTab(
|
||||
tab: StickerManagerTabType
|
||||
): SetStickerManagerTabAction {
|
||||
return {
|
||||
type: 'stickers/SET_STICKER_MANAGER_TAB',
|
||||
payload: tab,
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): StickersStateType {
|
||||
@ -389,6 +407,7 @@ export function getEmptyState(): StickersStateType {
|
||||
packs: {},
|
||||
recentStickers: [],
|
||||
blessedPacks: {},
|
||||
stickerManagerTab: 'all',
|
||||
};
|
||||
}
|
||||
|
||||
@ -557,6 +576,15 @@ export function reducer(
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'stickers/SET_STICKER_MANAGER_TAB') {
|
||||
const { payload: stickerManagerTab } = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
stickerManagerTab,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === ERASE_STORAGE_SERVICE) {
|
||||
const { packs } = state;
|
||||
|
||||
|
||||
@ -4,7 +4,10 @@
|
||||
import lodash, { type Dictionary } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import type { RecentStickerType } from '../../types/Stickers.preload.ts';
|
||||
import type {
|
||||
RecentStickerType,
|
||||
StickerManagerTabType,
|
||||
} from '../../types/Stickers.preload.ts';
|
||||
import {
|
||||
getLocalAttachmentUrl,
|
||||
AttachmentDisposition,
|
||||
@ -156,7 +159,9 @@ export const getReceivedStickerPacks = createSelector(
|
||||
return filterAndTransformPacks(
|
||||
packs,
|
||||
pack =>
|
||||
(pack.status === 'downloaded' || pack.status === 'pending') &&
|
||||
(pack.status === 'downloaded' ||
|
||||
pack.status === 'pending' ||
|
||||
pack.status === 'installed') &&
|
||||
!blessedPacks[pack.id],
|
||||
pack => pack.createdAt,
|
||||
blessedPacks
|
||||
@ -173,7 +178,7 @@ export const getBlessedStickerPacks = createSelector(
|
||||
): Array<StickerPackType> => {
|
||||
return filterAndTransformPacks(
|
||||
packs,
|
||||
pack => blessedPacks[pack.id] != null && pack.status !== 'installed',
|
||||
pack => blessedPacks[pack.id] != null,
|
||||
pack => pack.createdAt,
|
||||
blessedPacks
|
||||
);
|
||||
@ -195,3 +200,9 @@ export const getKnownStickerPacks = createSelector(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const getStickerManagerTab = createSelector(
|
||||
getStickers,
|
||||
(stickers: StickersStateType): StickerManagerTabType =>
|
||||
stickers.stickerManagerTab
|
||||
);
|
||||
|
||||
@ -43,6 +43,7 @@ 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';
|
||||
import { SmartStickerManagerHeader } from './StickerManagerHeader.preload.tsx';
|
||||
|
||||
const log = createLogger('ConversationPanel');
|
||||
|
||||
@ -305,6 +306,8 @@ const PanelContainer = forwardRef<HTMLDivElement, PanelPropsType>(
|
||||
let info: JSX.Element | undefined;
|
||||
if (panel.type === PanelType.AllMedia) {
|
||||
info = <SmartAllMediaHeader />;
|
||||
} else if (panel.type === PanelType.StickerManager) {
|
||||
info = <SmartStickerManagerHeader />;
|
||||
} else if (conversationTitle != null) {
|
||||
info = (
|
||||
<div className="ConversationPanel__header__info">
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
getInstalledStickerPacks,
|
||||
getKnownStickerPacks,
|
||||
getReceivedStickerPacks,
|
||||
getStickerManagerTab,
|
||||
} from '../selectors/stickers.std.ts';
|
||||
import { useStickersActions } from '../ducks/stickers.preload.ts';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals.preload.ts';
|
||||
@ -21,11 +22,13 @@ export const SmartStickerManager = memo(function SmartStickerManager() {
|
||||
const receivedPacks = useSelector(getReceivedStickerPacks);
|
||||
const installedPacks = useSelector(getInstalledStickerPacks);
|
||||
const knownPacks = useSelector(getKnownStickerPacks);
|
||||
const tab = useSelector(getStickerManagerTab);
|
||||
|
||||
const { downloadStickerPack, installStickerPack, uninstallStickerPack } =
|
||||
useStickersActions();
|
||||
const { closeStickerPackPreview } = useGlobalModalActions();
|
||||
const { showToast } = useToastActions();
|
||||
const { setStickerManagerTab } = useStickersActions();
|
||||
|
||||
return (
|
||||
<StickerManager
|
||||
@ -39,6 +42,8 @@ export const SmartStickerManager = memo(function SmartStickerManager() {
|
||||
receivedPacks={receivedPacks}
|
||||
uninstallStickerPack={uninstallStickerPack}
|
||||
showToast={showToast}
|
||||
setTab={setStickerManagerTab}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
24
ts/state/smart/StickerManagerHeader.preload.tsx
Normal file
24
ts/state/smart/StickerManagerHeader.preload.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StickerManagerHeader } from '../../components/stickers/StickerManagerHeader.dom.tsx';
|
||||
import { getIntl } from '../selectors/user.std.ts';
|
||||
import { getStickerManagerTab } from '../selectors/stickers.std.ts';
|
||||
import { useStickersActions } from '../ducks/stickers.preload.ts';
|
||||
|
||||
export const SmartStickerManagerHeader = memo(
|
||||
function SmartStickerManagerHeader() {
|
||||
const i18n = useSelector(getIntl);
|
||||
const tab = useSelector(getStickerManagerTab);
|
||||
const { setStickerManagerTab } = useStickersActions();
|
||||
|
||||
return (
|
||||
<StickerManagerHeader
|
||||
i18n={i18n}
|
||||
tab={tab}
|
||||
setTab={setStickerManagerTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -190,8 +190,8 @@ describe('stickers', function (this: Mocha.Suite) {
|
||||
'[data-testid=StickerManager]'
|
||||
);
|
||||
|
||||
debug('switching to Installed tab');
|
||||
await stickerManager.locator('.Tabs__tab >> "Installed"').click();
|
||||
debug('switching to My Stickers tab');
|
||||
await window.getByText('My Stickers').click();
|
||||
|
||||
{
|
||||
debug('installing first sticker pack via storage service');
|
||||
|
||||
@ -101,6 +101,8 @@ export type StickerPackPointerType = Readonly<{
|
||||
key: string;
|
||||
}>;
|
||||
|
||||
export type StickerManagerTabType = 'all' | 'my-stickers';
|
||||
|
||||
export const STICKERPACK_ID_BYTE_LEN = 16;
|
||||
export const STICKERPACK_KEY_BYTE_LEN = 32;
|
||||
|
||||
@ -193,6 +195,7 @@ export async function load(): Promise<void> {
|
||||
recentStickers,
|
||||
blessedPacks,
|
||||
installedPack: null,
|
||||
stickerManagerTab: 'all',
|
||||
};
|
||||
|
||||
packsToDownload = capturePacksToDownload(packs);
|
||||
|
||||
@ -141,6 +141,7 @@ window.testUtilities = {
|
||||
packs: {},
|
||||
recentStickers: [],
|
||||
blessedPacks: {},
|
||||
stickerManagerTab: 'all',
|
||||
},
|
||||
theme: ThemeType.dark,
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user