mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-08-19 20:57:57 +01:00
235 lines
7.9 KiB
TypeScript
235 lines
7.9 KiB
TypeScript
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
import { Dialog } from 'radix-ui';
|
|
import type { ReactNode, JSX } from 'react';
|
|
import { memo, useCallback, useEffect } from 'react';
|
|
import { type Placement, VisuallyHidden } from 'react-aria';
|
|
import { DialogTrigger } from 'react-aria-components';
|
|
import { createKeybindingsHandler } from 'tinykeys';
|
|
import { FunPickerTabKey } from './constants.dom.tsx';
|
|
import { FunPopover } from './base/FunPopover.dom.tsx';
|
|
import {
|
|
FunPickerTab,
|
|
FunTabList,
|
|
FunTabPanel,
|
|
FunTabs,
|
|
} from './base/FunTabs.dom.tsx';
|
|
import type { FunEmojiSelection } from './panels/FunPanelEmojis.dom.tsx';
|
|
import { FunPanelEmojis } from './panels/FunPanelEmojis.dom.tsx';
|
|
import type { FunGifSelection } from './panels/FunPanelGifs.dom.tsx';
|
|
import { FunPanelGifs } from './panels/FunPanelGifs.dom.tsx';
|
|
import type { FunStickerSelection } from './panels/FunPanelStickers.dom.tsx';
|
|
import { FunPanelStickers } from './panels/FunPanelStickers.dom.tsx';
|
|
import { useFunContext } from './FunProvider.dom.tsx';
|
|
import type { ThemeType } from '../../types/Util.std.ts';
|
|
import { FunErrorBoundary } from './base/FunErrorBoundary.dom.tsx';
|
|
import { strictAssert } from '../../util/assert.std.ts';
|
|
import { FunSticker } from './FunSticker.dom.tsx';
|
|
import { AxoIconButton } from '../../axo/AxoIconButton.dom.tsx';
|
|
import { AxoSymbol } from '../../axo/AxoSymbol.dom.tsx';
|
|
import type { LocalizerType } from '../../types/I18N.std.ts';
|
|
import { tw } from '../../axo/tw.dom.tsx';
|
|
|
|
/**
|
|
* FunPicker
|
|
*/
|
|
|
|
export type FunPickerProps = Readonly<{
|
|
open: boolean;
|
|
isReply: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
|
|
onSelectSticker: (stickerSelection: FunStickerSelection) => void;
|
|
onSelectGif: (gifSelection: FunGifSelection) => void;
|
|
onAddStickerPack: (() => void) | null;
|
|
placement?: Placement;
|
|
theme?: ThemeType;
|
|
children: ReactNode;
|
|
}>;
|
|
|
|
export const FunPicker = memo(function FunPicker(
|
|
props: FunPickerProps
|
|
): JSX.Element {
|
|
const { isReply, onOpenChange, onSelectSticker } = props;
|
|
const fun = useFunContext();
|
|
const {
|
|
i18n,
|
|
isStickerReplySendEnabled,
|
|
stagedStickerReply,
|
|
onOpenChange: onFunOpenChange,
|
|
onSelectSticker: onFunSelectSticker,
|
|
onChangeTab,
|
|
onStageStickerReply,
|
|
} = fun;
|
|
|
|
const isReplyForFunPanel = isStickerReplySendEnabled && isReply;
|
|
|
|
const handleOpenChange = useCallback(
|
|
(open: boolean) => {
|
|
onOpenChange(open);
|
|
onFunOpenChange(open);
|
|
},
|
|
[onOpenChange, onFunOpenChange]
|
|
);
|
|
|
|
const handleClose = useCallback(() => {
|
|
handleOpenChange(false);
|
|
}, [handleOpenChange]);
|
|
|
|
const handleCancelStickerReply = useCallback(() => {
|
|
onStageStickerReply(null);
|
|
}, [onStageStickerReply]);
|
|
|
|
const handleSendStickerReply = useCallback(() => {
|
|
strictAssert(stagedStickerReply, 'Staged sticker reply is required');
|
|
|
|
onFunSelectSticker(stagedStickerReply);
|
|
onSelectSticker(stagedStickerReply);
|
|
handleClose();
|
|
}, [stagedStickerReply, handleClose, onFunSelectSticker, onSelectSticker]);
|
|
|
|
useEffect(() => {
|
|
const onKeyDown = createKeybindingsHandler({
|
|
'$mod+Shift+J': () => {
|
|
onChangeTab(FunPickerTabKey.EmojisTab);
|
|
handleOpenChange(true);
|
|
},
|
|
'$mod+Shift+O': () => {
|
|
onChangeTab(FunPickerTabKey.StickersTab);
|
|
handleOpenChange(true);
|
|
},
|
|
'$mod+Shift+G': () => {
|
|
onChangeTab(FunPickerTabKey.GifsTab);
|
|
handleOpenChange(true);
|
|
},
|
|
});
|
|
window.addEventListener('keydown', onKeyDown);
|
|
return () => {
|
|
window.removeEventListener('keydown', onKeyDown);
|
|
};
|
|
}, [handleOpenChange, onChangeTab]);
|
|
|
|
return (
|
|
<DialogTrigger isOpen={props.open} onOpenChange={handleOpenChange}>
|
|
{props.children}
|
|
<FunPopover placement={props.placement} theme={props.theme}>
|
|
<FunTabs value={fun.tab} onChange={fun.onChangeTab}>
|
|
<StagedStickerReply
|
|
i18n={i18n}
|
|
selection={stagedStickerReply}
|
|
handleCancelStickerReply={handleCancelStickerReply}
|
|
handleSendStickerReply={handleSendStickerReply}
|
|
/>
|
|
<FunTabList>
|
|
<FunPickerTab id={FunPickerTabKey.EmojisTab}>
|
|
{i18n('icu:FunPicker__Tab--Emojis')}
|
|
</FunPickerTab>
|
|
<FunPickerTab id={FunPickerTabKey.StickersTab}>
|
|
{i18n('icu:FunPicker__Tab--Stickers')}
|
|
</FunPickerTab>
|
|
<FunPickerTab id={FunPickerTabKey.GifsTab}>
|
|
{i18n('icu:FunPicker__Tab--Gifs')}
|
|
</FunPickerTab>
|
|
</FunTabList>
|
|
<FunTabPanel id={FunPickerTabKey.EmojisTab}>
|
|
<FunErrorBoundary>
|
|
<FunPanelEmojis
|
|
onSelectEmoji={props.onSelectEmoji}
|
|
onClose={handleClose}
|
|
showCustomizePreferredReactionsButton={false}
|
|
closeOnSelect={false}
|
|
/>
|
|
</FunErrorBoundary>
|
|
</FunTabPanel>
|
|
<FunTabPanel id={FunPickerTabKey.StickersTab}>
|
|
<FunErrorBoundary>
|
|
<FunPanelStickers
|
|
isReply={isReplyForFunPanel}
|
|
showTimeStickers={false}
|
|
onSelectSticker={props.onSelectSticker}
|
|
onAddStickerPack={props.onAddStickerPack}
|
|
onClose={handleClose}
|
|
/>
|
|
</FunErrorBoundary>
|
|
</FunTabPanel>
|
|
<FunTabPanel id={FunPickerTabKey.GifsTab}>
|
|
<FunErrorBoundary>
|
|
<FunPanelGifs
|
|
onSelectGif={props.onSelectGif}
|
|
onClose={handleClose}
|
|
/>
|
|
</FunErrorBoundary>
|
|
</FunTabPanel>
|
|
</FunTabs>
|
|
</FunPopover>
|
|
</DialogTrigger>
|
|
);
|
|
});
|
|
|
|
const StagedStickerReply = memo(function StagedStickerReply(props: {
|
|
i18n: LocalizerType;
|
|
selection: FunStickerSelection | null;
|
|
handleCancelStickerReply: () => void;
|
|
handleSendStickerReply: () => void;
|
|
}): JSX.Element | null {
|
|
const { i18n, selection, handleCancelStickerReply, handleSendStickerReply } =
|
|
props;
|
|
if (!selection) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Dialog.Root modal={false} open onOpenChange={handleCancelStickerReply}>
|
|
<Dialog.Content onEscapeKeyDown={event => event.stopPropagation()}>
|
|
<div
|
|
className={tw(
|
|
'absolute legacy-z-index-above-above-base flex size-full items-center justify-center bg-material-primary backdrop-blur-thick'
|
|
)}
|
|
>
|
|
<div className={tw('size-50')}>
|
|
<FunSticker
|
|
role="img"
|
|
aria-label={i18n('icu:FunPicker__Label--Sticker')}
|
|
src={selection.stickerUrl}
|
|
size={200}
|
|
ignoreReducedMotion
|
|
/>
|
|
</div>
|
|
<div className={tw('absolute bottom-0 flex w-full items-center p-4')}>
|
|
<AxoIconButton.Root
|
|
size="md"
|
|
variant="strong-secondary"
|
|
symbol="x"
|
|
label={i18n('icu:FunPicker__Label--CancelStickerReply')}
|
|
onClick={handleCancelStickerReply}
|
|
tooltip={false}
|
|
/>
|
|
<span
|
|
className={tw(
|
|
'flex grow justify-center type-body-medium font-medium text-secondary'
|
|
)}
|
|
>
|
|
<AxoSymbol.InlineGlyph symbol="reply" label={null} />
|
|
|
|
{i18n('icu:FunPicker__Label--StickerReply')}
|
|
</span>
|
|
<AxoIconButton.Root
|
|
size="md"
|
|
variant="strong-primary"
|
|
symbol="send-fill"
|
|
label={i18n('icu:FunPicker__Label--SendStickerReply')}
|
|
onClick={handleSendStickerReply}
|
|
tooltip={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<VisuallyHidden>
|
|
<Dialog.Title>
|
|
{i18n('icu:FunPicker__Label--StickerReply')}
|
|
</Dialog.Title>
|
|
</VisuallyHidden>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|
|
);
|
|
});
|