mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-07-06 21:35:42 +01:00
Remove old emoji and sticker pickers
This commit is contained in:
@@ -3480,6 +3480,10 @@
|
||||
"messageformat": "Recently Used",
|
||||
"description": "FunPicker > Stickers Panel > Sub Nav > Category Label > Recents"
|
||||
},
|
||||
"icu:FunPanelStickers__SubNavButton--AddStickerPack": {
|
||||
"messageformat": "Add a sticker pack",
|
||||
"description": "FunPicker > Stickers Panel > Sub Nav > Button > Add a sticker pack (accessibility label, opens sticker manager)"
|
||||
},
|
||||
"icu:FunPanelStickers__SectionTitle--SearchResults": {
|
||||
"messageformat": "Search Results",
|
||||
"description": "FunPicker > Stickers Panel > Section Title > Search Results"
|
||||
|
||||
@@ -689,5 +689,8 @@
|
||||
"!node_modules/.cache",
|
||||
"sticker-creator/dist/**"
|
||||
]
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,9 +39,6 @@ const KnownConfigKeys = [
|
||||
'desktop.libsignalNet.shadowAuthChatWithNoise',
|
||||
'desktop.libsignalNet.chatPermessageDeflate',
|
||||
'desktop.libsignalNet.chatPermessageDeflate.prod',
|
||||
'desktop.funPicker', // alpha
|
||||
'desktop.funPicker.beta',
|
||||
'desktop.funPicker.prod',
|
||||
'desktop.pollReceive.alpha',
|
||||
'desktop.pollReceive.beta',
|
||||
'desktop.pollReceive.prod',
|
||||
|
||||
@@ -121,7 +121,6 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||
blockClient: action('block-client'),
|
||||
cancelPresenting: action('cancel-presenting'),
|
||||
renderDeviceSelection: () => <div />,
|
||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||
renderReactionPicker: () => <div />,
|
||||
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
||||
sendGroupCallReaction: action('send-group-call-reaction'),
|
||||
|
||||
@@ -47,7 +47,6 @@ import type { LocalizerType } from '../types/Util.js';
|
||||
import { missingCaseError } from '../util/missingCaseError.js';
|
||||
import { CallingToastProvider } from './CallingToast.js';
|
||||
import type { SmartReactionPicker } from '../state/smart/ReactionPicker.js';
|
||||
import type { Props as ReactionPickerProps } from './conversation/ReactionPicker.js';
|
||||
import { createLogger } from '../logging/log.js';
|
||||
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall.js';
|
||||
import { CallingAdhocCallInfo } from './CallingAdhocCallInfo.js';
|
||||
@@ -154,7 +153,7 @@ export type PropsType = {
|
||||
toggleSelfViewExpanded: () => unknown;
|
||||
toggleSettings: () => void;
|
||||
pauseVoiceNotePlayer: () => void;
|
||||
} & Pick<ReactionPickerProps, 'renderEmojiPicker'>;
|
||||
};
|
||||
|
||||
type ActiveCallManagerPropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
@@ -194,7 +193,6 @@ function ActiveCallManager({
|
||||
me,
|
||||
openSystemPreferencesAction,
|
||||
renderDeviceSelection,
|
||||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
removeClient,
|
||||
selectPresentingSource,
|
||||
@@ -478,7 +476,6 @@ function ActiveCallManager({
|
||||
isCallLinkAdmin={isCallLinkAdmin}
|
||||
me={me}
|
||||
openSystemPreferencesAction={openSystemPreferencesAction}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
renderReactionPicker={renderReactionPicker}
|
||||
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
||||
sendGroupCallReaction={sendGroupCallReaction}
|
||||
@@ -571,7 +568,6 @@ export function CallManager({
|
||||
playRingtone,
|
||||
removeClient,
|
||||
renderDeviceSelection,
|
||||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
ringingCall,
|
||||
selectPresentingSource,
|
||||
@@ -689,7 +685,6 @@ export function CallManager({
|
||||
pauseVoiceNotePlayer={pauseVoiceNotePlayer}
|
||||
removeClient={removeClient}
|
||||
renderDeviceSelection={renderDeviceSelection}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
renderReactionPicker={renderReactionPicker}
|
||||
selectPresentingSource={selectPresentingSource}
|
||||
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
||||
|
||||
@@ -244,7 +244,6 @@ const createProps = (
|
||||
serviceId: overrideProps.myAci ?? generateAci(),
|
||||
}),
|
||||
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||
renderReactionPicker: () => <div />,
|
||||
cancelPresenting: action('cancel-presenting'),
|
||||
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
||||
|
||||
@@ -74,7 +74,6 @@ import {
|
||||
} from './CallingToast.js';
|
||||
import { handleOutsideClick } from '../util/handleOutsideClick.js';
|
||||
import { Spinner } from './Spinner.js';
|
||||
import type { Props as ReactionPickerProps } from './conversation/ReactionPicker.js';
|
||||
import type { SmartReactionPicker } from '../state/smart/ReactionPicker.js';
|
||||
import {
|
||||
CallingRaisedHandsList,
|
||||
@@ -87,11 +86,12 @@ import {
|
||||
} from './CallReactionBurst.js';
|
||||
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall.js';
|
||||
import { assertDev, strictAssert } from '../util/assert.js';
|
||||
import { emojiToData } from './emoji/lib.js';
|
||||
import { CallingPendingParticipants } from './CallingPendingParticipants.js';
|
||||
import type { CallingImageDataCache } from './CallManager.js';
|
||||
import { FunStaticEmoji } from './fun/FunEmoji.js';
|
||||
import {
|
||||
getEmojiParentByKey,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
@@ -143,7 +143,7 @@ export type PropsType = {
|
||||
toggleSettings: () => void;
|
||||
changeCallView: (mode: CallViewMode) => void;
|
||||
setLocalAudioRemoteMuted: SetMutedByType;
|
||||
} & Pick<ReactionPickerProps, 'renderEmojiPicker'>;
|
||||
};
|
||||
|
||||
export const isInSpeakerView = (
|
||||
call: Pick<ActiveCallStateType, 'viewMode'> | undefined
|
||||
@@ -214,7 +214,6 @@ export function CallScreen({
|
||||
isCallLinkAdmin,
|
||||
me,
|
||||
openSystemPreferencesAction,
|
||||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
setGroupCallVideoRequest,
|
||||
sendGroupCallRaiseHand,
|
||||
@@ -1081,7 +1080,6 @@ export function CallScreen({
|
||||
value: emoji,
|
||||
});
|
||||
},
|
||||
renderEmojiPicker,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
@@ -1336,8 +1334,9 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||
);
|
||||
// Normalize skin tone emoji to calculate burst threshold, but save original
|
||||
// value to show in the burst animation
|
||||
const emojiData = emojiToData(value);
|
||||
const normalizedValue = emojiData?.unified ?? value;
|
||||
const emojiParentKey = getEmojiParentKeyByVariantKey(emojiVariantKey);
|
||||
const emojiParent = getEmojiParentByKey(emojiParentKey);
|
||||
const normalizedValue = emojiParent.value;
|
||||
reactionsShown.current.set(key, {
|
||||
value: normalizedValue,
|
||||
originalValue: value,
|
||||
|
||||
@@ -83,23 +83,11 @@ export default {
|
||||
draftText: undefined,
|
||||
getPreferredBadge: () => undefined,
|
||||
sortedGroupMembers: [],
|
||||
// EmojiButton
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
onEmojiSkinToneDefaultChange: action('onEmojiSkinToneDefaultChange'),
|
||||
recentEmojis: [],
|
||||
// FunPicker
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
emojiSkinToneDefault: EmojiSkinTone.Type1,
|
||||
// StickerButton
|
||||
knownPacks: [],
|
||||
receivedPacks: [],
|
||||
installedPacks: [],
|
||||
blessedPacks: [],
|
||||
recentStickers: [],
|
||||
clearInstalledStickerPack: action('clearInstalledStickerPack'),
|
||||
pushPanelForConversation: action('pushPanelForConversation'),
|
||||
sendStickerMessage: action('sendStickerMessage'),
|
||||
clearShowIntroduction: action('clearShowIntroduction'),
|
||||
showPickerHint: false,
|
||||
clearShowPickerHint: action('clearShowPickerHint'),
|
||||
// Message Requests
|
||||
conversationType: 'direct',
|
||||
acceptConversation: action('acceptConversation'),
|
||||
@@ -118,7 +106,7 @@ export default {
|
||||
showConversation: action('showConversation'),
|
||||
isSmsOnlyOrUnregistered: false,
|
||||
isFetchingUUID: false,
|
||||
renderSmartCompositionRecording: _ => <div>RECORDING</div>,
|
||||
renderSmartCompositionRecording: () => <div>RECORDING</div>,
|
||||
renderSmartCompositionRecordingDraft: _ => <div>RECORDING DRAFT</div>,
|
||||
// Select mode
|
||||
selectedMessageIds: undefined,
|
||||
@@ -149,14 +137,7 @@ export function StartingText(args: Props): JSX.Element {
|
||||
|
||||
export function StickerButton(args: Props): JSX.Element {
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
return (
|
||||
<CompositionArea
|
||||
{...args}
|
||||
theme={theme}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
knownPacks={[{} as any]}
|
||||
/>
|
||||
);
|
||||
return <CompositionArea {...args} theme={theme} />;
|
||||
}
|
||||
|
||||
export function MessageRequest(args: Props): JSX.Element {
|
||||
|
||||
@@ -14,13 +14,6 @@ import { RecordingState } from '../types/AudioRecorder.js';
|
||||
import type { imageToBlurHash } from '../util/imageToBlurHash.js';
|
||||
import { dropNull } from '../util/dropNull.js';
|
||||
import { Spinner } from './Spinner.js';
|
||||
import type {
|
||||
Props as EmojiButtonProps,
|
||||
EmojiButtonAPI,
|
||||
} from './emoji/EmojiButton.js';
|
||||
import { EmojiButton } from './emoji/EmojiButton.js';
|
||||
import type { Props as StickerButtonProps } from './stickers/StickerButton.js';
|
||||
import { StickerButton } from './stickers/StickerButton.js';
|
||||
import type {
|
||||
InputApi,
|
||||
Props as CompositionInputProps,
|
||||
@@ -48,7 +41,6 @@ import type {
|
||||
PushPanelForConversationActionType,
|
||||
ShowConversationType,
|
||||
} from '../state/ducks/conversations.js';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker.js';
|
||||
import type { LinkPreviewForUIType } from '../types/message/LinkPreviews.js';
|
||||
import { isSameLinkPreview } from '../types/message/LinkPreviews.js';
|
||||
|
||||
@@ -56,7 +48,6 @@ import { MandatoryProfileSharingActions } from './conversation/MandatoryProfileS
|
||||
import { MediaQualitySelector } from './MediaQualitySelector.js';
|
||||
import type { Props as QuoteProps } from './conversation/Quote.js';
|
||||
import { Quote } from './conversation/Quote.js';
|
||||
import { countStickers } from './stickers/lib.js';
|
||||
import {
|
||||
useAttachFileShortcut,
|
||||
useEditLastMessageSent,
|
||||
@@ -69,7 +60,6 @@ import { usePrevious } from '../hooks/usePrevious.js';
|
||||
import { PanelType } from '../types/Panels.js';
|
||||
import type { SmartCompositionRecordingDraftProps } from '../state/smart/CompositionRecordingDraft.js';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling.js';
|
||||
import type { SmartCompositionRecordingProps } from '../state/smart/CompositionRecording.js';
|
||||
import SelectModeActions from './conversation/SelectModeActions.js';
|
||||
import type { ShowToastAction } from '../state/ducks/toast.js';
|
||||
import type { DraftEditMessageType } from '../model-types.d.ts';
|
||||
@@ -84,9 +74,7 @@ import type { SmartDraftGifMessageSendModalProps } from '../state/smart/DraftGif
|
||||
import { strictAssert } from '../util/assert.js';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog.js';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.js';
|
||||
import type { StickerPackType, StickerType } from '../state/ducks/stickers.js';
|
||||
import { FunPickerButton } from './fun/FunButton.js';
|
||||
import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js';
|
||||
|
||||
export type OwnProps = Readonly<{
|
||||
acceptedMessageRequest: boolean | null;
|
||||
@@ -195,9 +183,7 @@ export type OwnProps = Readonly<{
|
||||
showConversation: ShowConversationType;
|
||||
startRecording: (id: string) => unknown;
|
||||
theme: ThemeType;
|
||||
renderSmartCompositionRecording: (
|
||||
props: SmartCompositionRecordingProps
|
||||
) => JSX.Element;
|
||||
renderSmartCompositionRecording: () => JSX.Element;
|
||||
renderSmartCompositionRecordingDraft: (
|
||||
props: SmartCompositionRecordingDraftProps
|
||||
) => JSX.Element | null;
|
||||
@@ -212,11 +198,8 @@ export type OwnProps = Readonly<{
|
||||
props: SmartDraftGifMessageSendModalProps | null
|
||||
) => void;
|
||||
|
||||
onPickEmoji: (e: EmojiPickDataType) => void;
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
// StickerButton
|
||||
installedPacks: ReadonlyArray<StickerPackType>;
|
||||
recentStickers: ReadonlyArray<StickerType>;
|
||||
}>;
|
||||
|
||||
export type Props = Pick<
|
||||
@@ -231,27 +214,6 @@ export type Props = Pick<
|
||||
| 'sendCounter'
|
||||
| 'sortedGroupMembers'
|
||||
> &
|
||||
Pick<
|
||||
EmojiButtonProps,
|
||||
| 'onPickEmoji'
|
||||
| 'onEmojiSkinToneDefaultChange'
|
||||
| 'recentEmojis'
|
||||
| 'emojiSkinToneDefault'
|
||||
> &
|
||||
Pick<
|
||||
StickerButtonProps,
|
||||
| 'knownPacks'
|
||||
| 'receivedPacks'
|
||||
| 'installedPack'
|
||||
| 'installedPacks'
|
||||
| 'blessedPacks'
|
||||
| 'recentStickers'
|
||||
| 'clearInstalledStickerPack'
|
||||
| 'showIntroduction'
|
||||
| 'clearShowIntroduction'
|
||||
| 'showPickerHint'
|
||||
| 'clearShowPickerHint'
|
||||
> &
|
||||
MessageRequestActionsProps &
|
||||
Pick<GroupV1DisabledActionsPropsType, 'showGV2MigrationDialog'> &
|
||||
Pick<GroupV2PendingApprovalActionsPropsType, 'cancelJoinRequest'> & {
|
||||
@@ -317,24 +279,10 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
ourConversationId,
|
||||
sendCounter,
|
||||
sortedGroupMembers,
|
||||
// EmojiButton
|
||||
onPickEmoji,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
recentEmojis,
|
||||
// FunPicker
|
||||
onSelectEmoji,
|
||||
emojiSkinToneDefault,
|
||||
// StickerButton
|
||||
knownPacks,
|
||||
receivedPacks,
|
||||
installedPack,
|
||||
installedPacks,
|
||||
blessedPacks,
|
||||
recentStickers,
|
||||
clearInstalledStickerPack,
|
||||
sendStickerMessage,
|
||||
showIntroduction,
|
||||
clearShowIntroduction,
|
||||
showPickerHint,
|
||||
clearShowPickerHint,
|
||||
// Message Requests
|
||||
acceptedMessageRequest,
|
||||
areWePending,
|
||||
@@ -383,7 +331,6 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
AttachmentDraftType | undefined
|
||||
>();
|
||||
const inputApiRef = useRef<InputApi | undefined>();
|
||||
const emojiButtonRef = useRef<EmojiButtonAPI | undefined>();
|
||||
const fileInputRef = useRef<null | HTMLInputElement>(null);
|
||||
|
||||
const handleForceSend = useCallback(() => {
|
||||
@@ -424,8 +371,6 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
return false;
|
||||
}
|
||||
|
||||
emojiButtonRef.current?.close();
|
||||
|
||||
if (editedMessageId) {
|
||||
sendEditedMessage(conversationId, {
|
||||
bodyRanges,
|
||||
@@ -524,14 +469,6 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
}
|
||||
}, [inputApiRef, focusCounter, previousFocusCounter]);
|
||||
|
||||
const withStickers =
|
||||
countStickers({
|
||||
knownPacks,
|
||||
blessedPacks,
|
||||
installedPacks,
|
||||
receivedPacks,
|
||||
}) > 0;
|
||||
|
||||
const previousMessageCompositionId = usePrevious(
|
||||
messageCompositionId,
|
||||
messageCompositionId
|
||||
@@ -554,16 +491,6 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
previousSendCounter,
|
||||
]);
|
||||
|
||||
const insertEmoji = useCallback(
|
||||
(e: EmojiPickDataType) => {
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.insertEmoji(e);
|
||||
onPickEmoji(e);
|
||||
}
|
||||
},
|
||||
[inputApiRef, onPickEmoji]
|
||||
);
|
||||
|
||||
// We want to reset the state of Quill only if:
|
||||
//
|
||||
// - Our other device edits the message (edit history length would change)
|
||||
@@ -627,12 +554,11 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
|
||||
const handleFunPickerSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
insertEmoji({
|
||||
shortName: emojiSelection.englishShortName,
|
||||
skinTone: emojiSelection.skinTone,
|
||||
});
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.insertEmoji(emojiSelection);
|
||||
}
|
||||
},
|
||||
[insertEmoji]
|
||||
[]
|
||||
);
|
||||
const handleFunPickerSelectSticker = useCallback(
|
||||
(stickerSelection: FunStickerSelection) => {
|
||||
@@ -720,34 +646,19 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
{i18n('icu:CompositionArea__ConfirmGifSelection__Body')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
{isFunPickerEnabled() && (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<FunPicker
|
||||
placement="top start"
|
||||
open={funPickerOpen}
|
||||
onOpenChange={handleFunPickerOpenChange}
|
||||
onSelectEmoji={handleFunPickerSelectEmoji}
|
||||
onSelectSticker={handleFunPickerSelectSticker}
|
||||
onSelectGif={handleFunPickerSelectGif}
|
||||
onAddStickerPack={handleFunPickerAddStickerPack}
|
||||
>
|
||||
<FunPickerButton i18n={i18n} />
|
||||
</FunPicker>
|
||||
</div>
|
||||
)}
|
||||
{!isFunPickerEnabled() && (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<EmojiButton
|
||||
emojiButtonApi={emojiButtonRef}
|
||||
i18n={i18n}
|
||||
onPickEmoji={insertEmoji}
|
||||
onClose={() => setComposerFocus(conversationId)}
|
||||
recentEmojis={recentEmojis}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="CompositionArea__button-cell">
|
||||
<FunPicker
|
||||
placement="top start"
|
||||
open={funPickerOpen}
|
||||
onOpenChange={handleFunPickerOpenChange}
|
||||
onSelectEmoji={handleFunPickerSelectEmoji}
|
||||
onSelectSticker={handleFunPickerSelectSticker}
|
||||
onSelectGif={handleFunPickerSelectGif}
|
||||
onAddStickerPack={handleFunPickerAddStickerPack}
|
||||
>
|
||||
<FunPickerButton i18n={i18n} />
|
||||
</FunPicker>
|
||||
</div>
|
||||
{showMediaQualitySelector ? (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<MediaQualitySelector
|
||||
@@ -821,36 +732,6 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
</>
|
||||
) : null;
|
||||
|
||||
const stickerButtonPlacement = large ? 'top-start' : 'top-end';
|
||||
const stickerButtonFragment =
|
||||
!isFunPickerEnabled() && !draftEditMessage && withStickers ? (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<StickerButton
|
||||
i18n={i18n}
|
||||
knownPacks={knownPacks}
|
||||
receivedPacks={receivedPacks}
|
||||
installedPack={installedPack}
|
||||
installedPacks={installedPacks}
|
||||
blessedPacks={blessedPacks}
|
||||
recentStickers={recentStickers}
|
||||
clearInstalledStickerPack={clearInstalledStickerPack}
|
||||
onClickAddPack={() =>
|
||||
pushPanelForConversation({
|
||||
type: PanelType.StickerManager,
|
||||
})
|
||||
}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
sendStickerMessage(conversationId, { packId, stickerId })
|
||||
}
|
||||
showIntroduction={showIntroduction}
|
||||
clearShowIntroduction={clearShowIntroduction}
|
||||
showPickerHint={showPickerHint}
|
||||
clearShowPickerHint={clearShowPickerHint}
|
||||
position={stickerButtonPlacement}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// Listen for cmd/ctrl-shift-x to toggle large composition mode
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
@@ -876,10 +757,6 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
};
|
||||
}, [platform, setLarge]);
|
||||
|
||||
const handleRecordingBeforeSend = useCallback(() => {
|
||||
emojiButtonRef.current?.close();
|
||||
}, [emojiButtonRef]);
|
||||
|
||||
const handleEscape = useCallback(() => {
|
||||
if (linkPreviewResult) {
|
||||
onCloseLinkPreview(conversationId);
|
||||
@@ -1060,9 +937,7 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
}
|
||||
|
||||
if (isRecording) {
|
||||
return renderSmartCompositionRecording({
|
||||
onBeforeSend: handleRecordingBeforeSend,
|
||||
});
|
||||
return renderSmartCompositionRecording();
|
||||
}
|
||||
|
||||
if (draftAttachments.length === 1 && isVoiceMessage(draftAttachments[0])) {
|
||||
@@ -1085,7 +960,6 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
i18n={i18n}
|
||||
imageSrc={attachmentToEdit.url}
|
||||
imageToBlurHash={imageToBlurHash}
|
||||
installedPacks={installedPacks}
|
||||
isCreatingStory={false}
|
||||
isFormattingEnabled={isFormattingEnabled}
|
||||
isSending={false}
|
||||
@@ -1120,11 +994,10 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
true
|
||||
);
|
||||
}}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
onTextTooLong={onTextTooLong}
|
||||
ourConversationId={ourConversationId}
|
||||
platform={platform}
|
||||
recentStickers={recentStickers}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
/>
|
||||
@@ -1213,7 +1086,7 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
onCloseLinkPreview={onCloseLinkPreview}
|
||||
onDirtyChange={setDirty}
|
||||
onEditorStateChange={onEditorStateChange}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
onSubmit={handleSubmit}
|
||||
onTextTooLong={onTextTooLong}
|
||||
ourConversationId={ourConversationId}
|
||||
@@ -1228,7 +1101,6 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
</div>
|
||||
{!large ? (
|
||||
<>
|
||||
{stickerButtonFragment}
|
||||
{!dirty ? micButtonFragment : null}
|
||||
{editMessageFragment}
|
||||
{attButton}
|
||||
@@ -1243,7 +1115,6 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
)}
|
||||
>
|
||||
{leftHandSideButtonsFragment}
|
||||
{stickerButtonFragment}
|
||||
{attButton}
|
||||
{!dirty ? micButtonFragment : null}
|
||||
{editMessageFragment}
|
||||
|
||||
@@ -39,7 +39,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => {
|
||||
large: overrideProps.large ?? false,
|
||||
onCloseLinkPreview: action('onCloseLinkPreview'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
onSubmit: action('onSubmit'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
ourConversationId: 'me',
|
||||
|
||||
@@ -23,8 +23,6 @@ import {
|
||||
import { MonospaceBlot } from '../quill/formatting/monospaceBlot.js';
|
||||
import { SpoilerBlot } from '../quill/formatting/spoilerBlot.js';
|
||||
import { EmojiBlot, EmojiCompletion } from '../quill/emoji/index.js';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker.js';
|
||||
import { convertShortName } from './emoji/lib.js';
|
||||
import type {
|
||||
DraftBodyRanges,
|
||||
HydratedBodyRangesType,
|
||||
@@ -79,12 +77,13 @@ import type { AutoSubstituteAsciiEmojisOptions } from '../quill/auto-substitute-
|
||||
import { AutoSubstituteAsciiEmojis } from '../quill/auto-substitute-ascii-emojis/index.js';
|
||||
import { dropNull } from '../util/dropNull.js';
|
||||
import { SimpleQuillWrapper } from './SimpleQuillWrapper.js';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.js';
|
||||
import { getEmojiVariantByKey, type EmojiSkinTone } from './fun/data/emojis.js';
|
||||
import { FUN_STATIC_EMOJI_CLASS } from './fun/FunEmoji.js';
|
||||
import { useFunEmojiSearch } from './fun/useFunEmojiSearch.js';
|
||||
import type { EmojiCompletionOptions } from '../quill/emoji/completion.js';
|
||||
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer.js';
|
||||
import { MAX_BODY_ATTACHMENT_BYTE_LENGTH } from '../util/longAttachment.js';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js';
|
||||
|
||||
const log = createLogger('CompositionInput');
|
||||
|
||||
@@ -106,7 +105,7 @@ Quill.register(
|
||||
|
||||
export type InputApi = {
|
||||
focus: () => void;
|
||||
insertEmoji: (e: EmojiPickDataType) => void;
|
||||
insertEmoji: (emojiSelection: FunEmojiSelection) => void;
|
||||
setContents: (
|
||||
text: string,
|
||||
draftBodyRanges?: HydratedBodyRangesType,
|
||||
@@ -144,7 +143,7 @@ export type Props = Readonly<{
|
||||
sendCounter: number;
|
||||
}): unknown;
|
||||
onTextTooLong(): unknown;
|
||||
onPickEmoji(o: EmojiPickDataType): unknown;
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
|
||||
onBlur?: () => unknown;
|
||||
onFocus?: () => unknown;
|
||||
onSubmit(
|
||||
@@ -184,7 +183,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
onCloseLinkPreview,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onPickEmoji,
|
||||
onSelectEmoji,
|
||||
onScroll,
|
||||
onSubmit,
|
||||
ourConversationId,
|
||||
@@ -285,7 +284,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
quill.focus();
|
||||
};
|
||||
|
||||
const insertEmoji = (e: EmojiPickDataType) => {
|
||||
const insertEmoji = (emojiSelection: FunEmojiSelection) => {
|
||||
const quill = quillRef.current;
|
||||
|
||||
if (quill === undefined) {
|
||||
@@ -299,12 +298,12 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const emoji = convertShortName(e.shortName, e.skinTone);
|
||||
const emojiVariant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
|
||||
const delta = new Delta()
|
||||
.retain(insertionRange.index)
|
||||
.delete(insertionRange.length)
|
||||
.insert({ emoji: { value: emoji } });
|
||||
.insert({ emoji: { value: emojiVariant.value } });
|
||||
|
||||
quill.updateContents(delta, 'user');
|
||||
quill.setSelection(insertionRange.index + 1, 0, 'user');
|
||||
@@ -768,7 +767,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
onChange,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onPickEmoji,
|
||||
onSelectEmoji,
|
||||
onShortKeyEnter,
|
||||
onTab,
|
||||
};
|
||||
@@ -842,8 +841,8 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
},
|
||||
emojiCompletion: {
|
||||
setEmojiPickerElement: setEmojiCompletionElement,
|
||||
onPickEmoji: (emoji: EmojiPickDataType) =>
|
||||
callbacksRef.current.onPickEmoji(emoji),
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) =>
|
||||
callbacksRef.current.onSelectEmoji(emojiSelection),
|
||||
emojiSkinToneDefault,
|
||||
emojiSearch,
|
||||
emojiLocalizer,
|
||||
|
||||
@@ -3,24 +3,20 @@
|
||||
|
||||
import React, { useRef, useCallback, useState } from 'react';
|
||||
import type { LocalizerType } from '../types/I18N.js';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker.js';
|
||||
import type { InputApi } from './CompositionInput.js';
|
||||
import { CompositionInput } from './CompositionInput.js';
|
||||
import { EmojiButton } from './emoji/EmojiButton.js';
|
||||
import {
|
||||
hydrateRanges,
|
||||
type DraftBodyRanges,
|
||||
type HydratedBodyRangesType,
|
||||
} from '../types/BodyRange.js';
|
||||
import type { ThemeType } from '../types/Util.js';
|
||||
import type { Props as EmojiButtonProps } from './emoji/EmojiButton.js';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges.js';
|
||||
import * as grapheme from '../util/grapheme.js';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.js';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.js';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.js';
|
||||
import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js';
|
||||
import type { GetConversationByIdType } from '../state/selectors/conversations.js';
|
||||
|
||||
export type CompositionTextAreaProps = {
|
||||
@@ -32,12 +28,13 @@ export type CompositionTextAreaProps = {
|
||||
placeholder?: string;
|
||||
whenToShowRemainingCount?: number;
|
||||
onScroll?: (ev: React.UIEvent<HTMLElement, UIEvent>) => void;
|
||||
onPickEmoji: (e: EmojiPickDataType) => void;
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
|
||||
onChange: (
|
||||
messageText: string,
|
||||
draftBodyRanges: HydratedBodyRangesType,
|
||||
caretLocation?: number | undefined
|
||||
) => void;
|
||||
emojiSkinToneDefault: EmojiSkinTone;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinToneDefault: EmojiSkinTone) => void;
|
||||
onSubmit: (
|
||||
message: string,
|
||||
@@ -51,7 +48,7 @@ export type CompositionTextAreaProps = {
|
||||
draftText: string;
|
||||
theme: ThemeType;
|
||||
conversationSelector: GetConversationByIdType;
|
||||
} & Pick<EmojiButtonProps, 'recentEmojis' | 'emojiSkinToneDefault'>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Essentially an HTML textarea but with support for emoji picker and
|
||||
@@ -69,15 +66,13 @@ export function CompositionTextArea({
|
||||
isFormattingEnabled,
|
||||
maxLength,
|
||||
onChange,
|
||||
onPickEmoji,
|
||||
onSelectEmoji,
|
||||
onScroll,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
onSubmit,
|
||||
onTextTooLong,
|
||||
ourConversationId,
|
||||
placeholder,
|
||||
platform,
|
||||
recentEmojis,
|
||||
emojiSkinToneDefault,
|
||||
theme,
|
||||
whenToShowRemainingCount = Infinity,
|
||||
@@ -88,25 +83,14 @@ export function CompositionTextArea({
|
||||
grapheme.count(draftText)
|
||||
);
|
||||
|
||||
const insertEmoji = useCallback(
|
||||
(e: EmojiPickDataType) => {
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.insertEmoji(e);
|
||||
onPickEmoji(e);
|
||||
}
|
||||
},
|
||||
[inputApiRef, onPickEmoji]
|
||||
);
|
||||
|
||||
const handleSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
const data: EmojiPickDataType = {
|
||||
shortName: emojiSelection.englishShortName,
|
||||
skinTone: emojiSelection.skinTone,
|
||||
};
|
||||
insertEmoji(data);
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.insertEmoji(emojiSelection);
|
||||
onSelectEmoji(emojiSelection);
|
||||
}
|
||||
},
|
||||
[insertEmoji]
|
||||
[onSelectEmoji]
|
||||
);
|
||||
|
||||
const focusTextEditInput = useCallback(() => {
|
||||
@@ -182,7 +166,7 @@ export function CompositionTextArea({
|
||||
large={false}
|
||||
moduleClassName="CompositionTextArea__input"
|
||||
onEditorStateChange={handleChange}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
onScroll={onScroll}
|
||||
onSubmit={onSubmit}
|
||||
onTextTooLong={onTextTooLong}
|
||||
@@ -205,27 +189,15 @@ export function CompositionTextArea({
|
||||
shouldHidePopovers={null}
|
||||
/>
|
||||
<div className="CompositionTextArea__emoji">
|
||||
{!isFunPickerEnabled() && (
|
||||
<EmojiButton
|
||||
i18n={i18n}
|
||||
onClose={focusTextEditInput}
|
||||
onPickEmoji={insertEmoji}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
recentEmojis={recentEmojis}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
/>
|
||||
)}
|
||||
{isFunPickerEnabled() && (
|
||||
<FunEmojiPicker
|
||||
placement="bottom"
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
)}
|
||||
<FunEmojiPicker
|
||||
placement="bottom"
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
</div>
|
||||
{maxLength !== undefined &&
|
||||
characterCount >= whenToShowRemainingCount && (
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import lodash from 'lodash';
|
||||
|
||||
import type { LocalizerType } from '../types/Util.js';
|
||||
@@ -13,17 +12,16 @@ import {
|
||||
ReactionPickerPickerEmojiButton,
|
||||
ReactionPickerPickerStyle,
|
||||
} from './ReactionPickerPicker.js';
|
||||
import { EmojiPicker } from './emoji/EmojiPicker.js';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../reactions/constants.js';
|
||||
import { convertShortName } from './emoji/lib.js';
|
||||
import { offsetDistanceModifier } from '../util/popperUtil.js';
|
||||
import { handleOutsideClick } from '../util/handleOutsideClick.js';
|
||||
import { EmojiSkinTone, getEmojiVariantByKey } from './fun/data/emojis.js';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS } from '../reactions/constants.js';
|
||||
import {
|
||||
EmojiSkinTone,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
} from './fun/data/emojis.js';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.js';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js';
|
||||
import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js';
|
||||
|
||||
const { isEqual, noop } = lodash;
|
||||
const { isEqual } = lodash;
|
||||
|
||||
export type PropsType = {
|
||||
draftPreferredReactions: ReadonlyArray<string>;
|
||||
@@ -52,57 +50,17 @@ export function CustomizingPreferredReactionsModal({
|
||||
hadSaveError,
|
||||
i18n,
|
||||
isSaving,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
originalPreferredReactions,
|
||||
recentEmojis,
|
||||
replaceSelectedDraftEmoji,
|
||||
resetDraftEmoji,
|
||||
savePreferredReactions,
|
||||
selectDraftEmojiToBeReplaced,
|
||||
selectedDraftEmojiIndex,
|
||||
}: Readonly<PropsType>): JSX.Element {
|
||||
const [referenceElement, setReferenceElement] =
|
||||
useState<null | HTMLDivElement>(null);
|
||||
const pickerRef = useRef<HTMLDivElement>(null);
|
||||
const [popperElement, setPopperElement] = useState<null | HTMLDivElement>(
|
||||
null
|
||||
);
|
||||
const emojiPickerPopper = usePopper(referenceElement, popperElement, {
|
||||
placement: 'bottom',
|
||||
modifiers: [
|
||||
offsetDistanceModifier(8),
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: { altAxis: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const isSomethingSelected = selectedDraftEmojiIndex !== undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSomethingSelected) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
return handleOutsideClick(
|
||||
target => {
|
||||
if (
|
||||
target instanceof Element &&
|
||||
target.closest('[data-fun-overlay]') != null
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
deselectDraftEmoji();
|
||||
return true;
|
||||
},
|
||||
{
|
||||
containerElements: [popperElement, pickerRef],
|
||||
name: 'CustomizingPreferredReactionsModal.draftEmoji',
|
||||
}
|
||||
);
|
||||
}, [isSomethingSelected, popperElement, deselectDraftEmoji]);
|
||||
|
||||
const hasChanged = !isEqual(
|
||||
originalPreferredReactions,
|
||||
draftPreferredReactions
|
||||
@@ -110,9 +68,13 @@ export function CustomizingPreferredReactionsModal({
|
||||
const canReset =
|
||||
!isSaving &&
|
||||
!isEqual(
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(shortName =>
|
||||
convertShortName(shortName, emojiSkinToneDefault ?? EmojiSkinTone.None)
|
||||
),
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS.map(parentKey => {
|
||||
const variant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
parentKey,
|
||||
emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
);
|
||||
return variant.value;
|
||||
}),
|
||||
draftPreferredReactions
|
||||
);
|
||||
const canSave = !isSaving && hasChanged;
|
||||
@@ -168,7 +130,6 @@ export function CustomizingPreferredReactionsModal({
|
||||
<ReactionPickerPicker
|
||||
isSomethingSelected={isSomethingSelected}
|
||||
pickerStyle={ReactionPickerPickerStyle.Menu}
|
||||
ref={setReferenceElement}
|
||||
>
|
||||
{draftPreferredReactions.map((emoji, index) => {
|
||||
return (
|
||||
@@ -199,31 +160,6 @@ export function CustomizingPreferredReactionsModal({
|
||||
? i18n('icu:CustomizingPreferredReactions__had-save-error')
|
||||
: i18n('icu:CustomizingPreferredReactions__subtitle')}
|
||||
</div>
|
||||
{!isFunPickerEnabled() && isSomethingSelected && (
|
||||
<div
|
||||
ref={setPopperElement}
|
||||
style={emojiPickerPopper.styles.popper}
|
||||
{...emojiPickerPopper.attributes.popper}
|
||||
>
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
onPickEmoji={pickedEmoji => {
|
||||
const emoji = convertShortName(
|
||||
pickedEmoji.shortName,
|
||||
pickedEmoji.skinTone
|
||||
);
|
||||
replaceSelectedDraftEmoji(emoji);
|
||||
}}
|
||||
recentEmojis={recentEmojis}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
onClose={() => {
|
||||
deselectDraftEmoji();
|
||||
}}
|
||||
wasInvokedFromKeyboard={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -249,26 +185,19 @@ function CustomizingPreferredReactionsModalItem(props: {
|
||||
[onDeselect]
|
||||
);
|
||||
|
||||
const button = (
|
||||
<ReactionPickerPickerEmojiButton
|
||||
emoji={props.emoji}
|
||||
onClick={props.onSelect}
|
||||
isSelected={props.isSelected}
|
||||
/>
|
||||
return (
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
placement="bottom"
|
||||
onSelectEmoji={props.onSelectEmoji}
|
||||
closeOnSelect
|
||||
>
|
||||
<ReactionPickerPickerEmojiButton
|
||||
emoji={props.emoji}
|
||||
onClick={props.onSelect}
|
||||
isSelected={props.isSelected}
|
||||
/>
|
||||
</FunEmojiPicker>
|
||||
);
|
||||
|
||||
if (isFunPickerEnabled()) {
|
||||
return (
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
placement="bottom"
|
||||
onSelectEmoji={props.onSelectEmoji}
|
||||
closeOnSelect
|
||||
>
|
||||
{button}
|
||||
</FunEmojiPicker>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ function RenderCompositionTextArea(props: SmartCompositionTextAreaProps) {
|
||||
i18n={i18n}
|
||||
isActive
|
||||
isFormattingEnabled
|
||||
onPickEmoji={action('onPickEmoji')}
|
||||
onSelectEmoji={action('onSelectEmoji')}
|
||||
onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')}
|
||||
onTextTooLong={action('onTextTooLong')}
|
||||
ourConversationId="me"
|
||||
|
||||
@@ -64,7 +64,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
i18n={i18n}
|
||||
isActive
|
||||
isFormattingEnabled
|
||||
onPickEmoji={action('onPickEmoji')}
|
||||
onSelectEmoji={action('onSelectEmoji')}
|
||||
onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')}
|
||||
onTextTooLong={action('onTextTooLong')}
|
||||
ourConversationId="me"
|
||||
|
||||
@@ -7,7 +7,6 @@ import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { PropsType } from './MediaEditor.js';
|
||||
import { MediaEditor } from './MediaEditor.js';
|
||||
import { Stickers, installedPacks } from '../test-helpers/getStickerPacks.js';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
@@ -24,15 +23,13 @@ export default {
|
||||
i18n,
|
||||
imageToBlurHash: input => Promise.resolve(input.toString()),
|
||||
imageSrc: IMAGE_2,
|
||||
installedPacks,
|
||||
isFormattingEnabled: true,
|
||||
isSending: false,
|
||||
onClose: action('onClose'),
|
||||
onDone: action('onDone'),
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
platform: 'darwin',
|
||||
recentStickers: [Stickers.wide, Stickers.tall, Stickers.abe],
|
||||
emojiSkinToneDefault: EmojiSkinTone.None,
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
+42
-128
@@ -14,10 +14,6 @@ import { createPortal } from 'react-dom';
|
||||
import { fabric } from 'fabric';
|
||||
import { useSelector } from 'react-redux';
|
||||
import lodash from 'lodash';
|
||||
import type {
|
||||
EmojiPickDataType,
|
||||
Props as EmojiPickerProps,
|
||||
} from './emoji/EmojiPicker.js';
|
||||
import type { DraftBodyRanges } from '../types/BodyRange.js';
|
||||
import type { ImageStateType } from '../mediaEditor/ImageStateType.js';
|
||||
import type {
|
||||
@@ -26,7 +22,6 @@ import type {
|
||||
} from './CompositionInput.js';
|
||||
import type { LocalizerType } from '../types/Util.js';
|
||||
import type { MIMEType } from '../types/MIME.js';
|
||||
import type { Props as StickerButtonProps } from './stickers/StickerButton.js';
|
||||
import type { imageToBlurHash } from '../util/imageToBlurHash.js';
|
||||
import { MediaEditorFabricAnalogTimeSticker } from '../mediaEditor/MediaEditorFabricAnalogTimeSticker.js';
|
||||
import { MediaEditorFabricCropRect } from '../mediaEditor/MediaEditorFabricCropRect.js';
|
||||
@@ -44,12 +39,10 @@ import { createLogger } from '../logging/log.js';
|
||||
import { Button, ButtonVariant } from './Button.js';
|
||||
import { CompositionInput } from './CompositionInput.js';
|
||||
import { ContextMenu } from './ContextMenu.js';
|
||||
import { EmojiButton } from './emoji/EmojiButton.js';
|
||||
import { IMAGE_PNG } from '../types/MIME.js';
|
||||
import { SizeObserver } from '../hooks/useSizeObserver.js';
|
||||
import { Slider } from './Slider.js';
|
||||
import { Spinner } from './Spinner.js';
|
||||
import { StickerButton } from './stickers/StickerButton.js';
|
||||
import { Theme } from '../util/theme.js';
|
||||
import { ThemeType } from '../types/Util.js';
|
||||
import { arrow } from '../util/keyboard.js';
|
||||
@@ -60,7 +53,6 @@ import { hydrateRanges } from '../types/BodyRange.js';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard.js';
|
||||
import { useFabricHistory } from '../mediaEditor/useFabricHistory.js';
|
||||
import { usePortal } from '../hooks/usePortal.js';
|
||||
import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.js';
|
||||
import {
|
||||
FunEmojiPickerButton,
|
||||
@@ -94,20 +86,19 @@ export type PropsType = {
|
||||
imageToBlurHash: typeof imageToBlurHash;
|
||||
onClose: () => unknown;
|
||||
onDone: (result: MediaEditorResultType) => unknown;
|
||||
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
|
||||
Pick<
|
||||
CompositionInputProps,
|
||||
| 'draftText'
|
||||
| 'draftBodyRanges'
|
||||
| 'getPreferredBadge'
|
||||
| 'isFormattingEnabled'
|
||||
| 'onPickEmoji'
|
||||
| 'onTextTooLong'
|
||||
| 'ourConversationId'
|
||||
| 'platform'
|
||||
| 'sortedGroupMembers'
|
||||
> &
|
||||
Omit<EmojiPickerProps, 'wasInvokedFromKeyboard'>;
|
||||
} & Pick<
|
||||
CompositionInputProps,
|
||||
| 'draftText'
|
||||
| 'draftBodyRanges'
|
||||
| 'getPreferredBadge'
|
||||
| 'isFormattingEnabled'
|
||||
| 'emojiSkinToneDefault'
|
||||
| 'onSelectEmoji'
|
||||
| 'onTextTooLong'
|
||||
| 'ourConversationId'
|
||||
| 'platform'
|
||||
| 'sortedGroupMembers'
|
||||
>;
|
||||
|
||||
const INITIAL_IMAGE_STATE: ImageStateType = {
|
||||
angle: 0,
|
||||
@@ -171,20 +162,12 @@ export function MediaEditor({
|
||||
draftBodyRanges,
|
||||
getPreferredBadge,
|
||||
isFormattingEnabled,
|
||||
onPickEmoji,
|
||||
emojiSkinToneDefault,
|
||||
onSelectEmoji,
|
||||
onTextTooLong,
|
||||
ourConversationId,
|
||||
platform,
|
||||
sortedGroupMembers,
|
||||
|
||||
// EmojiPickerProps
|
||||
onEmojiSkinToneDefaultChange,
|
||||
recentEmojis,
|
||||
emojiSkinToneDefault,
|
||||
|
||||
// StickerButtonProps
|
||||
installedPacks,
|
||||
recentStickers,
|
||||
...props
|
||||
}: PropsType): JSX.Element | null {
|
||||
const [fabricCanvas, setFabricCanvas] = useState<fabric.Canvas | undefined>();
|
||||
@@ -209,23 +192,6 @@ export function MediaEditor({
|
||||
const [imageState, setImageState] =
|
||||
useState<ImageStateType>(INITIAL_IMAGE_STATE);
|
||||
|
||||
const closeEmojiPickerAndFocusComposer = useCallback(() => {
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.focus();
|
||||
}
|
||||
setEmojiPickerOpen(false);
|
||||
}, [inputApiRef]);
|
||||
|
||||
const insertEmoji = useCallback(
|
||||
(e: EmojiPickDataType) => {
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.insertEmoji(e);
|
||||
onPickEmoji(e);
|
||||
}
|
||||
},
|
||||
[inputApiRef, onPickEmoji]
|
||||
);
|
||||
|
||||
const handleEmojiPickerOpenChange = useCallback((open: boolean) => {
|
||||
setEmojiPickerOpen(open);
|
||||
}, []);
|
||||
@@ -234,16 +200,11 @@ export function MediaEditor({
|
||||
setStickerPickerOpen(open);
|
||||
}, []);
|
||||
|
||||
const handleSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
const data: EmojiPickDataType = {
|
||||
shortName: emojiSelection.englishShortName,
|
||||
skinTone: emojiSelection.skinTone,
|
||||
};
|
||||
insertEmoji(data);
|
||||
},
|
||||
[insertEmoji]
|
||||
);
|
||||
const handleSelectEmoji = useCallback((emojiSelection: FunEmojiSelection) => {
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.insertEmoji(emojiSelection);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePickSticker = useCallback(
|
||||
(_packId: string, _stickerId: number, src: string) => {
|
||||
@@ -1333,48 +1294,17 @@ export function MediaEditor({
|
||||
}}
|
||||
type="button"
|
||||
/>
|
||||
{!isFunPickerEnabled() && (
|
||||
<StickerButton
|
||||
blessedPacks={[]}
|
||||
className={classNames({
|
||||
MediaEditor__control: true,
|
||||
'MediaEditor__control--sticker': true,
|
||||
})}
|
||||
onOpenStateChanged={value => {
|
||||
setStickerPickerOpen(value);
|
||||
}}
|
||||
clearInstalledStickerPack={noop}
|
||||
clearShowIntroduction={() => {
|
||||
// We're using this as a callback for when the sticker button
|
||||
// is pressed.
|
||||
fabricCanvas?.discardActiveObject();
|
||||
setEditMode(undefined);
|
||||
}}
|
||||
clearShowPickerHint={noop}
|
||||
i18n={i18n}
|
||||
installedPacks={installedPacks}
|
||||
knownPacks={[]}
|
||||
onPickSticker={handlePickSticker}
|
||||
onPickTimeSticker={handlePickTimeSticker}
|
||||
receivedPacks={[]}
|
||||
recentStickers={recentStickers}
|
||||
showPickerHint={false}
|
||||
theme={Theme.Dark}
|
||||
/>
|
||||
)}
|
||||
{isFunPickerEnabled() && (
|
||||
<FunStickerPicker
|
||||
open={stickerPickerOpen}
|
||||
onOpenChange={handleStickerPickerOpenChange}
|
||||
onSelectSticker={handleSelectSticker}
|
||||
showTimeStickers
|
||||
onSelectTimeSticker={handlePickTimeSticker}
|
||||
placement="top"
|
||||
theme={ThemeType.dark}
|
||||
>
|
||||
<FunStickerPickerButton i18n={i18n} />
|
||||
</FunStickerPicker>
|
||||
)}
|
||||
<FunStickerPicker
|
||||
open={stickerPickerOpen}
|
||||
onOpenChange={handleStickerPickerOpenChange}
|
||||
onSelectSticker={handleSelectSticker}
|
||||
showTimeStickers
|
||||
onSelectTimeSticker={handlePickTimeSticker}
|
||||
placement="top"
|
||||
theme={ThemeType.dark}
|
||||
>
|
||||
<FunStickerPickerButton i18n={i18n} />
|
||||
</FunStickerPicker>
|
||||
</div>
|
||||
<div className="MediaEditor__tools-row-2">
|
||||
<div className="MediaEditor__tools--input dark-theme">
|
||||
@@ -1393,7 +1323,7 @@ export function MediaEditor({
|
||||
setCaption(messageText);
|
||||
}}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault ?? null}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
onSubmit={noop}
|
||||
onTextTooLong={onTextTooLong}
|
||||
ourConversationId={ourConversationId}
|
||||
@@ -1414,32 +1344,16 @@ export function MediaEditor({
|
||||
// link previews not displayed with media
|
||||
linkPreviewResult={null}
|
||||
>
|
||||
{!isFunPickerEnabled() && (
|
||||
<EmojiButton
|
||||
className="StoryViewsNRepliesModal__emoji-button"
|
||||
i18n={i18n}
|
||||
onPickEmoji={insertEmoji}
|
||||
onOpen={() => setEmojiPickerOpen(true)}
|
||||
onClose={closeEmojiPickerAndFocusComposer}
|
||||
recentEmojis={recentEmojis}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={
|
||||
onEmojiSkinToneDefaultChange
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isFunPickerEnabled() && (
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
placement="top"
|
||||
theme={ThemeType.dark}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
)}
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
placement="top"
|
||||
theme={ThemeType.dark}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
</CompositionInput>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -193,10 +193,8 @@ function renderProfileEditor({
|
||||
'markCompletedUsernameLinkOnboarding'
|
||||
)}
|
||||
onProfileChanged={action('onProfileChanged')}
|
||||
onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')}
|
||||
openUsernameReservationModal={action('openUsernameReservationModal')}
|
||||
profileAvatarUrl={undefined}
|
||||
recentEmojis={[]}
|
||||
renderUsernameEditor={() => <div />}
|
||||
replaceAvatar={action('replaceAvatar')}
|
||||
resetUsernameLink={action('resetUsernameLink')}
|
||||
|
||||
@@ -40,7 +40,7 @@ import { assertDev } from '../util/assert.js';
|
||||
import { I18n } from './I18n.js';
|
||||
import { FunSkinTonesList } from './fun/FunSkinTones.js';
|
||||
import {
|
||||
emojiParentKeyConstant,
|
||||
EMOJI_PARENT_KEY_CONSTANTS,
|
||||
type EmojiSkinTone,
|
||||
} from './fun/data/emojis.js';
|
||||
import {
|
||||
@@ -1159,8 +1159,7 @@ export function Preferences({
|
||||
right={
|
||||
<FunSkinTonesList
|
||||
i18n={i18n}
|
||||
// Raised Hand
|
||||
emoji={emojiParentKeyConstant('\u{270B}')}
|
||||
emoji={EMOJI_PARENT_KEY_CONSTANTS.RAISED_HAND}
|
||||
skinTone={emojiSkinToneDefault}
|
||||
onSelectSkinTone={onEmojiSkinToneDefaultChange}
|
||||
/>
|
||||
|
||||
@@ -63,13 +63,11 @@ export default {
|
||||
usernameEditState: UsernameEditState.Editing,
|
||||
usernameLinkState: UsernameLinkState.Ready,
|
||||
|
||||
recentEmojis: [],
|
||||
emojiSkinToneDefault: EmojiSkinTone.None,
|
||||
userAvatarData: [],
|
||||
username: undefined,
|
||||
|
||||
onProfileChanged: action('onProfileChanged'),
|
||||
onEmojiSkinToneDefaultChange: action('onEmojiSkinToneDefaultChange'),
|
||||
saveAttachment: action('saveAttachment'),
|
||||
setUsernameLinkColor: action('setUsernameLinkColor'),
|
||||
showToast: action('showToast'),
|
||||
@@ -107,17 +105,12 @@ function renderUsernameEditor(props: { onClose: () => void }): JSX.Element {
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<PropsType> = args => {
|
||||
const [emojiSkinToneDefault, setEmojiSkinToneDefault] = useState(
|
||||
EmojiSkinTone.None
|
||||
);
|
||||
const [editState, setEditState] = useState(args.editState);
|
||||
|
||||
return (
|
||||
<ProfileEditor
|
||||
{...args}
|
||||
editState={editState}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault}
|
||||
renderUsernameEditor={renderUsernameEditor}
|
||||
setEditState={setEditState}
|
||||
/>
|
||||
|
||||
@@ -16,13 +16,11 @@ import { AvatarColors } from '../types/Colors.js';
|
||||
import { AvatarEditor } from './AvatarEditor.js';
|
||||
import { AvatarPreview } from './AvatarPreview.js';
|
||||
import { Button, ButtonVariant } from './Button.js';
|
||||
import { EmojiButton, EmojiButtonVariant } from './emoji/EmojiButton.js';
|
||||
import { Input } from './Input.js';
|
||||
import { PanelRow } from './conversation/conversation-details/PanelRow.js';
|
||||
import { UsernameEditState } from '../state/ducks/usernameEnums.js';
|
||||
import { ToastType } from '../types/Toast.js';
|
||||
import { getEmojiData, unifiedToEmoji } from './emoji/lib.js';
|
||||
import { assertDev, strictAssert } from '../util/assert.js';
|
||||
import { assertDev } from '../util/assert.js';
|
||||
import { missingCaseError } from '../util/missingCaseError.js';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog.js';
|
||||
import { ContextMenu } from './ContextMenu.js';
|
||||
@@ -38,17 +36,15 @@ import { offsetDistanceModifier } from '../util/popperUtil.js';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion.js';
|
||||
import { FunStaticEmoji } from './fun/FunEmoji.js';
|
||||
import {
|
||||
EMOJI_PARENT_KEY_CONSTANTS,
|
||||
EmojiSkinTone,
|
||||
getEmojiParentKeyByEnglishShortName,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiEnglishShortName,
|
||||
isEmojiVariantValue,
|
||||
} from './fun/data/emojis.js';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.js';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.js';
|
||||
import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js';
|
||||
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer.js';
|
||||
import { PreferencesContent } from './Preferences.js';
|
||||
import { ProfileEditorPage } from '../types/Nav.js';
|
||||
@@ -61,8 +57,6 @@ import type {
|
||||
ReplaceAvatarActionType,
|
||||
SaveAvatarToDiskActionType,
|
||||
} from '../types/Avatar.js';
|
||||
import type { Props as EmojiButtonProps } from './emoji/EmojiButton.js';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker.js';
|
||||
import type { LocalizerType } from '../types/Util.js';
|
||||
import type {
|
||||
ConversationType,
|
||||
@@ -71,7 +65,7 @@ import type {
|
||||
} from '../state/ducks/conversations.js';
|
||||
import type { UsernameLinkState } from '../state/ducks/usernameEnums.js';
|
||||
import type { ShowToastAction } from '../state/ducks/toast.js';
|
||||
import type { EmojiVariantKey } from './fun/data/emojis.js';
|
||||
import type { EmojiParentKey, EmojiVariantKey } from './fun/data/emojis.js';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard.js';
|
||||
|
||||
@@ -93,6 +87,7 @@ export type PropsDataType = {
|
||||
color?: AvatarColorType;
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
conversationId: string;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
familyName?: string;
|
||||
firstName: string;
|
||||
hasCompletedUsernameLinkOnboarding: boolean;
|
||||
@@ -107,12 +102,11 @@ export type PropsDataType = {
|
||||
usernameLinkColor?: number;
|
||||
usernameLinkCorrupted: boolean;
|
||||
usernameLinkState: UsernameLinkState;
|
||||
} & Pick<EmojiButtonProps, 'recentEmojis' | 'emojiSkinToneDefault'>;
|
||||
};
|
||||
|
||||
type PropsActionType = {
|
||||
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||
markCompletedUsernameLinkOnboarding: () => void;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
|
||||
replaceAvatar: ReplaceAvatarActionType;
|
||||
saveAttachment: SaveAttachmentActionCreatorType;
|
||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
@@ -129,30 +123,30 @@ export type PropsType = PropsDataType & PropsActionType & PropsExternalType;
|
||||
|
||||
type DefaultBio = {
|
||||
i18nLabel: string;
|
||||
shortName: string;
|
||||
emojiParentKey: EmojiParentKey;
|
||||
};
|
||||
|
||||
function getDefaultBios(i18n: LocalizerType): Array<DefaultBio> {
|
||||
return [
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--speak-freely'),
|
||||
shortName: 'wave',
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.WAVING_HAND,
|
||||
},
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--encrypted'),
|
||||
shortName: 'zipper_mouth_face',
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.ZIPPER_MOUTH_FACE,
|
||||
},
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--free-to-chat'),
|
||||
shortName: '+1',
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
},
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--coffee-lover'),
|
||||
shortName: 'coffee',
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.HOT_BEVERAGE,
|
||||
},
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--taking-break'),
|
||||
shortName: 'mobile_phone_off',
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.MOBILE_PHONE_OFF,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -185,10 +179,8 @@ export function ProfileEditor({
|
||||
editState,
|
||||
markCompletedUsernameLinkOnboarding,
|
||||
onProfileChanged,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
openUsernameReservationModal,
|
||||
profileAvatarUrl,
|
||||
recentEmojis,
|
||||
renderUsernameEditor,
|
||||
replaceAvatar,
|
||||
resetUsernameLink,
|
||||
@@ -279,21 +271,6 @@ export function ProfileEditor({
|
||||
setEmojiPickerOpen(open);
|
||||
}, []);
|
||||
|
||||
// To make EmojiButton re-render less often
|
||||
const setAboutEmoji = useCallback(
|
||||
(ev: EmojiPickDataType) => {
|
||||
const emojiData = getEmojiData(
|
||||
ev.shortName,
|
||||
emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
);
|
||||
setStagedProfile(profileData => ({
|
||||
...profileData,
|
||||
aboutEmoji: unifiedToEmoji(emojiData.unified),
|
||||
}));
|
||||
},
|
||||
[setStagedProfile, emojiSkinToneDefault]
|
||||
);
|
||||
|
||||
const handleSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
const emojiVariant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
@@ -470,32 +447,18 @@ export function ProfileEditor({
|
||||
i18n={i18n}
|
||||
icon={
|
||||
<div className="module-composition-area__button-cell">
|
||||
{!isFunPickerEnabled() && (
|
||||
<EmojiButton
|
||||
variant={EmojiButtonVariant.ProfileEditor}
|
||||
closeOnPick
|
||||
emoji={stagedProfile.aboutEmoji}
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
placement="bottom"
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
closeOnSelect
|
||||
>
|
||||
<FunEmojiPickerButton
|
||||
i18n={i18n}
|
||||
onPickEmoji={setAboutEmoji}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
recentEmojis={recentEmojis}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
selectedEmoji={stagedAboutEmojiVariantKey}
|
||||
/>
|
||||
)}
|
||||
{isFunPickerEnabled() && (
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
placement="bottom"
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
closeOnSelect
|
||||
>
|
||||
<FunEmojiPickerButton
|
||||
i18n={i18n}
|
||||
selectedEmoji={stagedAboutEmojiVariantKey}
|
||||
/>
|
||||
</FunEmojiPicker>
|
||||
)}
|
||||
</FunEmojiPicker>
|
||||
</div>
|
||||
}
|
||||
maxLengthCount={140}
|
||||
@@ -523,22 +486,15 @@ export function ProfileEditor({
|
||||
/>
|
||||
|
||||
{defaultBios.map(defaultBio => {
|
||||
strictAssert(
|
||||
isEmojiEnglishShortName(defaultBio.shortName),
|
||||
'Must be valid english short name'
|
||||
);
|
||||
const emojiParentKey = getEmojiParentKeyByEnglishShortName(
|
||||
defaultBio.shortName
|
||||
);
|
||||
const emojiVariant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
emojiParentKey,
|
||||
defaultBio.emojiParentKey,
|
||||
emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelRow
|
||||
className="ProfileEditor__row"
|
||||
key={defaultBio.shortName}
|
||||
key={defaultBio.emojiParentKey}
|
||||
icon={
|
||||
<div className="ProfileEditor__icon--container">
|
||||
<BioEmoji emoji={emojiVariant.key} />
|
||||
@@ -546,14 +502,9 @@ export function ProfileEditor({
|
||||
}
|
||||
label={defaultBio.i18nLabel}
|
||||
onClick={() => {
|
||||
const emojiData = getEmojiData(
|
||||
defaultBio.shortName,
|
||||
emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
);
|
||||
|
||||
setStagedProfile(profileData => ({
|
||||
...profileData,
|
||||
aboutEmoji: unifiedToEmoji(emojiData.unified),
|
||||
aboutEmoji: emojiVariant.value,
|
||||
aboutText: defaultBio.i18nLabel,
|
||||
}));
|
||||
}}
|
||||
|
||||
@@ -16,7 +16,6 @@ export default {
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
i18n,
|
||||
close: action('close'),
|
||||
hasInstalledStickers: overrideProps.hasInstalledStickers === true || false,
|
||||
platform: overrideProps.platform || 'other',
|
||||
});
|
||||
|
||||
@@ -29,8 +28,3 @@ export function Mac(): JSX.Element {
|
||||
const props = createProps({ platform: 'darwin' });
|
||||
return <ShortcutGuide {...props} />;
|
||||
}
|
||||
|
||||
export function HasStickers(): JSX.Element {
|
||||
const props = createProps({ hasInstalledStickers: true });
|
||||
return <ShortcutGuide {...props} />;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useRestoreFocus } from '../hooks/useRestoreFocus.js';
|
||||
import type { LocalizerType } from '../types/Util.js';
|
||||
|
||||
export type Props = {
|
||||
hasInstalledStickers: boolean;
|
||||
platform: string;
|
||||
readonly close: () => unknown;
|
||||
readonly i18n: LocalizerType;
|
||||
@@ -363,7 +362,7 @@ function getCallingShortcuts(i18n: LocalizerType): Array<ShortcutType> {
|
||||
}
|
||||
|
||||
export function ShortcutGuide(props: Props): JSX.Element {
|
||||
const { i18n, close, hasInstalledStickers, platform } = props;
|
||||
const { i18n, close, platform } = props;
|
||||
const isMacOS = platform === 'darwin';
|
||||
|
||||
// Restore focus on teardown
|
||||
@@ -395,13 +394,6 @@ export function ShortcutGuide(props: Props): JSX.Element {
|
||||
</div>
|
||||
<div className="module-shortcut-guide__section-list">
|
||||
{getNavigationShortcuts(i18n).map((shortcut, index) => {
|
||||
if (
|
||||
!hasInstalledStickers &&
|
||||
shortcut.description === 'Keyboard--open-sticker-chooser'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return renderShortcut(shortcut, index, isMacOS, i18n);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { LocalizerType } from '../types/Util.js';
|
||||
import { ShortcutGuide } from './ShortcutGuide.js';
|
||||
|
||||
export type PropsType = {
|
||||
hasInstalledStickers: boolean;
|
||||
platform: string;
|
||||
readonly closeShortcutGuideModal: () => unknown;
|
||||
readonly i18n: LocalizerType;
|
||||
@@ -16,8 +15,7 @@ export type PropsType = {
|
||||
export const ShortcutGuideModal = React.memo(function ShortcutGuideModalInner(
|
||||
props: PropsType
|
||||
) {
|
||||
const { i18n, closeShortcutGuideModal, hasInstalledStickers, platform } =
|
||||
props;
|
||||
const { i18n, closeShortcutGuideModal, platform } = props;
|
||||
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -36,7 +34,6 @@ export const ShortcutGuideModal = React.memo(function ShortcutGuideModalInner(
|
||||
<div className="module-shortcut-guide-container">
|
||||
<ShortcutGuide
|
||||
close={closeShortcutGuideModal}
|
||||
hasInstalledStickers={hasInstalledStickers}
|
||||
i18n={i18n}
|
||||
platform={platform}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,6 @@ export default {
|
||||
hasFirstStoryPostExperience: false,
|
||||
i18n,
|
||||
imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
installedPacks: [],
|
||||
isSending: false,
|
||||
linkPreview: undefined,
|
||||
me: getDefaultConversation(),
|
||||
@@ -39,12 +38,9 @@ export default {
|
||||
onDistributionListCreated: undefined,
|
||||
onHideMyStoriesFrom: action('onHideMyStoriesFrom'),
|
||||
onSend: action('onSend'),
|
||||
onEmojiSkinToneDefaultChange: action('onEmojiSkinToneDefaultChange'),
|
||||
onUseEmoji: action('onUseEmoji'),
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
onViewersUpdated: action('onViewersUpdated'),
|
||||
processAttachment: undefined,
|
||||
recentEmojis: [],
|
||||
recentStickers: [],
|
||||
sendStoryModalOpenStateChanged: action('sendStoryModalOpenStateChanged'),
|
||||
setMyStoriesToAllSignalConnections: action(
|
||||
'setMyStoriesToAllSignalConnections'
|
||||
|
||||
@@ -9,11 +9,9 @@ import type { AttachmentType } from '../types/Attachment.js';
|
||||
import type { LinkPreviewSourceType } from '../types/LinkPreview.js';
|
||||
import type { LinkPreviewForUIType } from '../types/message/LinkPreviews.js';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util.js';
|
||||
import type { Props as StickerButtonProps } from './stickers/StickerButton.js';
|
||||
import type { PropsType as SendStoryModalPropsType } from './SendStoryModal.js';
|
||||
import type { StoryDistributionIdString } from '../types/StoryDistributionId.js';
|
||||
import type { imageToBlurHash } from '../util/imageToBlurHash.js';
|
||||
import type { PropsType as TextStoryCreatorPropsType } from './TextStoryCreator.js';
|
||||
import type { PropsType as MediaEditorPropsType } from './MediaEditor.js';
|
||||
|
||||
import { TEXT_ATTACHMENT } from '../types/MIME.js';
|
||||
@@ -63,42 +61,35 @@ export type PropsType = {
|
||||
processAttachment: typeof processAttachment;
|
||||
sendStoryModalOpenStateChanged: (isOpen: boolean) => unknown;
|
||||
theme: ThemeType;
|
||||
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
|
||||
Pick<
|
||||
SendStoryModalPropsType,
|
||||
| 'candidateConversations'
|
||||
| 'distributionLists'
|
||||
| 'getPreferredBadge'
|
||||
| 'groupConversations'
|
||||
| 'groupStories'
|
||||
| 'hasFirstStoryPostExperience'
|
||||
| 'me'
|
||||
| 'ourConversationId'
|
||||
| 'onDeleteList'
|
||||
| 'onDistributionListCreated'
|
||||
| 'onHideMyStoriesFrom'
|
||||
| 'onRemoveMembers'
|
||||
| 'onRepliesNReactionsChanged'
|
||||
| 'onSelectedStoryList'
|
||||
| 'onViewersUpdated'
|
||||
| 'setMyStoriesToAllSignalConnections'
|
||||
| 'signalConnections'
|
||||
| 'toggleGroupsForStorySend'
|
||||
| 'mostRecentActiveStoryTimestampByGroupOrDistributionList'
|
||||
| 'toggleSignalConnectionsModal'
|
||||
| 'onMediaPlaybackStart'
|
||||
> &
|
||||
Pick<
|
||||
TextStoryCreatorPropsType,
|
||||
| 'onUseEmoji'
|
||||
| 'emojiSkinToneDefault'
|
||||
| 'onEmojiSkinToneDefaultChange'
|
||||
| 'recentEmojis'
|
||||
> &
|
||||
} & Pick<
|
||||
SendStoryModalPropsType,
|
||||
| 'candidateConversations'
|
||||
| 'distributionLists'
|
||||
| 'getPreferredBadge'
|
||||
| 'groupConversations'
|
||||
| 'groupStories'
|
||||
| 'hasFirstStoryPostExperience'
|
||||
| 'me'
|
||||
| 'ourConversationId'
|
||||
| 'onDeleteList'
|
||||
| 'onDistributionListCreated'
|
||||
| 'onHideMyStoriesFrom'
|
||||
| 'onRemoveMembers'
|
||||
| 'onRepliesNReactionsChanged'
|
||||
| 'onSelectedStoryList'
|
||||
| 'onViewersUpdated'
|
||||
| 'setMyStoriesToAllSignalConnections'
|
||||
| 'signalConnections'
|
||||
| 'toggleGroupsForStorySend'
|
||||
| 'mostRecentActiveStoryTimestampByGroupOrDistributionList'
|
||||
| 'toggleSignalConnectionsModal'
|
||||
| 'onMediaPlaybackStart'
|
||||
> &
|
||||
Pick<
|
||||
MediaEditorPropsType,
|
||||
| 'isFormattingEnabled'
|
||||
| 'onPickEmoji'
|
||||
| 'emojiSkinToneDefault'
|
||||
| 'onSelectEmoji'
|
||||
| 'onTextTooLong'
|
||||
| 'platform'
|
||||
| 'sortedGroupMembers'
|
||||
@@ -115,7 +106,6 @@ export function StoryCreator({
|
||||
hasFirstStoryPostExperience,
|
||||
i18n,
|
||||
imageToBlurHash,
|
||||
installedPacks,
|
||||
isFormattingEnabled,
|
||||
isSending,
|
||||
linkPreview,
|
||||
@@ -126,20 +116,16 @@ export function StoryCreator({
|
||||
onDistributionListCreated,
|
||||
onHideMyStoriesFrom,
|
||||
onMediaPlaybackStart,
|
||||
onPickEmoji,
|
||||
onSelectEmoji,
|
||||
onRemoveMembers,
|
||||
onRepliesNReactionsChanged,
|
||||
onSelectedStoryList,
|
||||
onSend,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
onTextTooLong,
|
||||
onUseEmoji,
|
||||
onViewersUpdated,
|
||||
ourConversationId,
|
||||
platform,
|
||||
processAttachment,
|
||||
recentEmojis,
|
||||
recentStickers,
|
||||
sendStoryModalOpenStateChanged,
|
||||
setMyStoriesToAllSignalConnections,
|
||||
signalConnections,
|
||||
@@ -274,7 +260,6 @@ export function StoryCreator({
|
||||
i18n={i18n}
|
||||
imageSrc={attachmentUrl}
|
||||
imageToBlurHash={imageToBlurHash}
|
||||
installedPacks={installedPacks}
|
||||
isCreatingStory
|
||||
isFormattingEnabled={isFormattingEnabled}
|
||||
isSending={isSending}
|
||||
@@ -297,11 +282,10 @@ export function StoryCreator({
|
||||
setBodyRanges(captionBodyRanges);
|
||||
setIsReadyToSend(true);
|
||||
}}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
onTextTooLong={onTextTooLong}
|
||||
ourConversationId={ourConversationId}
|
||||
platform={platform}
|
||||
recentStickers={recentStickers}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
draftText={null}
|
||||
@@ -323,10 +307,7 @@ export function StoryCreator({
|
||||
});
|
||||
setIsReadyToSend(true);
|
||||
}}
|
||||
onUseEmoji={onUseEmoji}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
recentEmojis={recentEmojis}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
/>
|
||||
)}
|
||||
</>,
|
||||
|
||||
@@ -44,13 +44,11 @@ export default {
|
||||
onHideStory: action('onHideStory'),
|
||||
onReactToStory: action('onReactToStory'),
|
||||
onReplyToStory: action('onReplyToStory'),
|
||||
onEmojiSkinToneDefaultChange: action('onEmojiSkinToneDefaultChange'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
onUseEmoji: action('onUseEmoji'),
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
onMediaPlaybackStart: action('onMediaPlaybackStart'),
|
||||
preferredReactionEmoji: DEFAULT_PREFERRED_REACTION_EMOJI,
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||
retryMessageSend: action('retryMessageSend'),
|
||||
showToast: action('showToast'),
|
||||
emojiSkinToneDefault: EmojiSkinTone.None,
|
||||
|
||||
@@ -12,9 +12,7 @@ import type {
|
||||
ConversationType,
|
||||
SaveAttachmentActionCreatorType,
|
||||
} from '../state/ducks/conversations.js';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker.js';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges.js';
|
||||
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker.js';
|
||||
import type { ReplyStateType, StoryViewType } from '../types/Stories.js';
|
||||
import type { StoryDistributionIdString } from '../types/StoryDistributionId.js';
|
||||
import type { ShowToastAction } from '../state/ducks/toast.js';
|
||||
@@ -54,6 +52,7 @@ import { RenderLocation } from './conversation/MessageTextRenderer.js';
|
||||
import { arrow } from '../util/keyboard.js';
|
||||
import { StoryProgressSegment } from './StoryProgressSegment.js';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.js';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js';
|
||||
|
||||
const log = createLogger('StoryViewer');
|
||||
|
||||
@@ -94,7 +93,6 @@ export type PropsType = {
|
||||
numStories: number;
|
||||
onGoToConversation: (conversationId: string) => unknown;
|
||||
onHideStory: (conversationId: string) => unknown;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => unknown;
|
||||
onTextTooLong: () => unknown;
|
||||
onReactToStory: (emoji: string, story: StoryViewType) => unknown;
|
||||
onReplyToStory: (
|
||||
@@ -103,14 +101,12 @@ export type PropsType = {
|
||||
timestamp: number,
|
||||
story: StoryViewType
|
||||
) => unknown;
|
||||
onUseEmoji: (_: EmojiPickDataType) => unknown;
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => unknown;
|
||||
onMediaPlaybackStart: () => void;
|
||||
ourConversationId: string | undefined;
|
||||
platform: string;
|
||||
preferredReactionEmoji: ReadonlyArray<string>;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
recentEmojis?: ReadonlyArray<string>;
|
||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
||||
replyState?: ReplyStateType;
|
||||
retryMessageSend: (messageId: string) => unknown;
|
||||
saveAttachment: SaveAttachmentActionCreatorType;
|
||||
@@ -158,16 +154,13 @@ export function StoryViewer({
|
||||
onHideStory,
|
||||
onReactToStory,
|
||||
onReplyToStory,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
onTextTooLong,
|
||||
onUseEmoji,
|
||||
onSelectEmoji,
|
||||
onMediaPlaybackStart,
|
||||
ourConversationId,
|
||||
platform,
|
||||
preferredReactionEmoji,
|
||||
queueStoryDownload,
|
||||
recentEmojis,
|
||||
renderEmojiPicker,
|
||||
replyState,
|
||||
retryMessageSend,
|
||||
saveAttachment,
|
||||
@@ -967,13 +960,10 @@ export function StoryViewer({
|
||||
}
|
||||
onReplyToStory(message, replyBodyRanges, replyTimestamp, story);
|
||||
}}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
onTextTooLong={onTextTooLong}
|
||||
onUseEmoji={onUseEmoji}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
ourConversationId={ourConversationId}
|
||||
preferredReactionEmoji={preferredReactionEmoji}
|
||||
recentEmojis={recentEmojis}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
replies={replies}
|
||||
showContactModal={showContactModal}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
|
||||
@@ -36,13 +36,11 @@ export default {
|
||||
i18n,
|
||||
platform: 'darwin',
|
||||
onClose: action('onClose'),
|
||||
onEmojiSkinToneDefaultChange: action('onEmojiSkinToneDefaultChange'),
|
||||
onReact: action('onReact'),
|
||||
onReply: action('onReply'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
onUseEmoji: action('onUseEmoji'),
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
preferredReactionEmoji: DEFAULT_PREFERRED_REACTION_EMOJI,
|
||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||
replies: [],
|
||||
views: [],
|
||||
viewTarget: StoryViewTargetType.Views,
|
||||
|
||||
@@ -13,16 +13,13 @@ import lodash from 'lodash';
|
||||
import type { DraftBodyRanges } from '../types/BodyRange.js';
|
||||
import type { LocalizerType } from '../types/Util.js';
|
||||
import type { ConversationType } from '../state/ducks/conversations.js';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker.js';
|
||||
import type { InputApi } from './CompositionInput.js';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges.js';
|
||||
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker.js';
|
||||
import type { ReplyType, StorySendStateType } from '../types/Stories.js';
|
||||
import { StoryViewTargetType } from '../types/Stories.js';
|
||||
import { Avatar, AvatarSize } from './Avatar.js';
|
||||
import { CompositionInput } from './CompositionInput.js';
|
||||
import { ContactName } from './conversation/ContactName.js';
|
||||
import { EmojiButton } from './emoji/EmojiButton.js';
|
||||
import { Emojify } from './conversation/Emojify.js';
|
||||
import { Message, TextDirection } from './conversation/Message.js';
|
||||
import { MessageTimestamp } from './conversation/MessageTimestamp.js';
|
||||
@@ -37,7 +34,6 @@ import { shouldNeverBeCalled } from '../util/shouldNeverBeCalled.js';
|
||||
import { ContextMenu } from './ContextMenu.js';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog.js';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.js';
|
||||
import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.js';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.js';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js';
|
||||
@@ -113,13 +109,10 @@ export type PropsType = {
|
||||
bodyRanges: DraftBodyRanges,
|
||||
timestamp: number
|
||||
) => unknown;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
|
||||
onTextTooLong: () => unknown;
|
||||
onUseEmoji: (_: EmojiPickDataType) => unknown;
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => unknown;
|
||||
ourConversationId: string | undefined;
|
||||
preferredReactionEmoji: ReadonlyArray<string>;
|
||||
recentEmojis?: ReadonlyArray<string>;
|
||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
||||
replies: ReadonlyArray<ReplyType>;
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
@@ -145,13 +138,10 @@ export function StoryViewsNRepliesModal({
|
||||
onClose,
|
||||
onReact,
|
||||
onReply,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
onTextTooLong,
|
||||
onUseEmoji,
|
||||
onSelectEmoji,
|
||||
ourConversationId,
|
||||
preferredReactionEmoji,
|
||||
recentEmojis,
|
||||
renderEmojiPicker,
|
||||
replies,
|
||||
showContactModal,
|
||||
emojiSkinToneDefault,
|
||||
@@ -203,32 +193,11 @@ export function StoryViewsNRepliesModal({
|
||||
setEmojiPickerOpen(open);
|
||||
}, []);
|
||||
|
||||
const focusComposer = useCallback(() => {
|
||||
const handleSelectEmoji = useCallback((emojiSelection: FunEmojiSelection) => {
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.focus();
|
||||
inputApiRef.current.insertEmoji(emojiSelection);
|
||||
}
|
||||
}, [inputApiRef]);
|
||||
|
||||
const insertEmoji = useCallback(
|
||||
(e: EmojiPickDataType) => {
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.insertEmoji(e);
|
||||
onUseEmoji(e);
|
||||
}
|
||||
},
|
||||
[inputApiRef, onUseEmoji]
|
||||
);
|
||||
|
||||
const handleSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
const data: EmojiPickDataType = {
|
||||
shortName: emojiSelection.englishShortName,
|
||||
skinTone: emojiSelection.skinTone,
|
||||
};
|
||||
insertEmoji(data);
|
||||
},
|
||||
[insertEmoji]
|
||||
);
|
||||
}, []);
|
||||
|
||||
let composerElement: JSX.Element | undefined;
|
||||
|
||||
@@ -271,9 +240,7 @@ export function StoryViewsNRepliesModal({
|
||||
}
|
||||
onReact(emoji);
|
||||
}}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
preferredReactionEmoji={preferredReactionEmoji}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
theme={ThemeType.dark}
|
||||
/>
|
||||
<div className="StoryViewsNRepliesModal__compose-container">
|
||||
@@ -290,7 +257,7 @@ export function StoryViewsNRepliesModal({
|
||||
onEditorStateChange={({ messageText }) => {
|
||||
setMessageBodyText(messageText);
|
||||
}}
|
||||
onPickEmoji={onUseEmoji}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
onSubmit={(...args) => {
|
||||
inputApiRef.current?.reset();
|
||||
shouldScrollToBottomRef.current = true;
|
||||
@@ -318,29 +285,16 @@ export function StoryViewsNRepliesModal({
|
||||
shouldHidePopovers={null}
|
||||
linkPreviewResult={null}
|
||||
>
|
||||
{!isFunPickerEnabled() && (
|
||||
<EmojiButton
|
||||
className="StoryViewsNRepliesModal__emoji-button"
|
||||
i18n={i18n}
|
||||
onPickEmoji={insertEmoji}
|
||||
onClose={focusComposer}
|
||||
recentEmojis={recentEmojis}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
/>
|
||||
)}
|
||||
{isFunPickerEnabled() && (
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
placement="top"
|
||||
theme={ThemeType.dark}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
)}
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
placement="top"
|
||||
theme={ThemeType.dark}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
</CompositionInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,11 @@ import classNames from 'classnames';
|
||||
import lodash from 'lodash';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { FocusScope } from 'react-aria';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker.js';
|
||||
import type { LinkPreviewForUIType } from '../types/message/LinkPreviews.js';
|
||||
import { ThemeType, type LocalizerType } from '../types/Util.js';
|
||||
import type { Props as EmojiButtonPropsType } from './emoji/EmojiButton.js';
|
||||
import type { TextAttachmentType } from '../types/Attachment.js';
|
||||
import { Button, ButtonVariant } from './Button.js';
|
||||
import { ContextMenu } from './ContextMenu.js';
|
||||
import { EmojiButton } from './emoji/EmojiButton.js';
|
||||
import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview.js';
|
||||
import type { MaybeGrabLinkPreviewOptionsType } from '../types/LinkPreview.js';
|
||||
import { Input } from './Input.js';
|
||||
@@ -26,7 +23,6 @@ import {
|
||||
COLOR_WHITE_INT,
|
||||
getBackgroundColor,
|
||||
} from '../util/getStoryBackground.js';
|
||||
import { convertShortName } from './emoji/lib.js';
|
||||
import { objectMap } from '../util/objectMap.js';
|
||||
import { handleOutsideClick } from '../util/handleOutsideClick.js';
|
||||
import { Spinner } from './Spinner.js';
|
||||
@@ -34,7 +30,6 @@ import { FunEmojiPicker } from './fun/FunEmojiPicker.js';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js';
|
||||
import { getEmojiVariantByKey } from './fun/data/emojis.js';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.js';
|
||||
import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard.js';
|
||||
|
||||
const { noop } = lodash;
|
||||
@@ -50,11 +45,8 @@ export type PropsType = {
|
||||
linkPreview?: LinkPreviewForUIType;
|
||||
onClose: () => unknown;
|
||||
onDone: (textAttachment: TextAttachmentType) => unknown;
|
||||
onUseEmoji: (_: EmojiPickDataType) => unknown;
|
||||
} & Pick<
|
||||
EmojiButtonPropsType,
|
||||
'onEmojiSkinToneDefaultChange' | 'recentEmojis' | 'emojiSkinToneDefault'
|
||||
>;
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => unknown;
|
||||
};
|
||||
|
||||
enum LinkPreviewApplied {
|
||||
None = 'None',
|
||||
@@ -145,10 +137,7 @@ export function TextStoryCreator({
|
||||
linkPreview,
|
||||
onClose,
|
||||
onDone,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
onUseEmoji,
|
||||
recentEmojis,
|
||||
emojiSkinToneDefault,
|
||||
onSelectEmoji,
|
||||
}: PropsType): JSX.Element {
|
||||
const tryClose = useRef<() => void | undefined>();
|
||||
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({
|
||||
@@ -350,20 +339,25 @@ export function TextStoryCreator({
|
||||
setEmojiPickerOpen(open);
|
||||
}, []);
|
||||
|
||||
const handleSelectEmoji = useCallback((emojiSelection: FunEmojiSelection) => {
|
||||
const emojiVariant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
const emojiValue = emojiVariant.value;
|
||||
const handleSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
const emojiVariant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
const emojiValue = emojiVariant.value;
|
||||
|
||||
setText(originalText => {
|
||||
const insertAt =
|
||||
textEditorRef.current?.selectionEnd ?? originalText.length;
|
||||
onSelectEmoji(emojiSelection);
|
||||
|
||||
const before = originalText.substr(0, insertAt);
|
||||
const after = originalText.substr(insertAt, originalText.length);
|
||||
setText(originalText => {
|
||||
const insertAt =
|
||||
textEditorRef.current?.selectionEnd ?? originalText.length;
|
||||
|
||||
return `${before}${emojiValue}${after}`;
|
||||
});
|
||||
}, []);
|
||||
const before = originalText.substr(0, insertAt);
|
||||
const after = originalText.substr(insertAt, originalText.length);
|
||||
|
||||
return `${before}${emojiValue}${after}`;
|
||||
});
|
||||
},
|
||||
[onSelectEmoji]
|
||||
);
|
||||
|
||||
return (
|
||||
<FocusScope contain restoreFocus>
|
||||
@@ -466,43 +460,16 @@ export function TextStoryCreator({
|
||||
}}
|
||||
type="button"
|
||||
/>
|
||||
{!isFunPickerEnabled() && (
|
||||
<EmojiButton
|
||||
className="StoryCreator__emoji-button"
|
||||
i18n={i18n}
|
||||
onPickEmoji={data => {
|
||||
onUseEmoji(data);
|
||||
const emoji = convertShortName(
|
||||
data.shortName,
|
||||
data.skinTone
|
||||
);
|
||||
const insertAt =
|
||||
textEditorRef.current?.selectionEnd ?? text.length;
|
||||
setText(
|
||||
originalText =>
|
||||
`${originalText.substr(
|
||||
0,
|
||||
insertAt
|
||||
)}${emoji}${originalText.substr(insertAt, text.length)}`
|
||||
);
|
||||
}}
|
||||
recentEmojis={recentEmojis}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
/>
|
||||
)}
|
||||
{isFunPickerEnabled() && (
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
placement="top"
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
theme={ThemeType.dark}
|
||||
closeOnSelect
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
)}
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
placement="top"
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
theme={ThemeType.dark}
|
||||
closeOnSelect
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
</div>
|
||||
) : (
|
||||
<div className="StoryCreator__toolbar--space" />
|
||||
|
||||
@@ -117,7 +117,6 @@ const defaultMessageProps: TimelineMessagesProps = {
|
||||
previews: [],
|
||||
reactToMessage: action('default--reactToMessage'),
|
||||
readStatus: ReadStatus.Read,
|
||||
renderEmojiPicker: () => <div />,
|
||||
renderReactionPicker: () => <div />,
|
||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||
setMessageToEdit: action('setMessageToEdit'),
|
||||
|
||||
@@ -6,29 +6,10 @@ import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { Props as ReactionPickerProps } from './ReactionPicker.js';
|
||||
import { ReactionPicker } from './ReactionPicker.js';
|
||||
import { EmojiPicker } from '../emoji/EmojiPicker.js';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../../reactions/constants.js';
|
||||
import { EmojiSkinTone } from '../fun/data/emojis.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
const renderEmojiPicker: ReactionPickerProps['renderEmojiPicker'] = ({
|
||||
onClose,
|
||||
onPickEmoji,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
ref,
|
||||
}) => (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
ref={ref}
|
||||
onClose={onClose}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
wasInvokedFromKeyboard={false}
|
||||
/>
|
||||
);
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/ReactionPicker',
|
||||
} satisfies Meta<ReactionPickerProps>;
|
||||
@@ -38,12 +19,7 @@ export function Base(): JSX.Element {
|
||||
<ReactionPicker
|
||||
i18n={i18n}
|
||||
onPick={action('onPick')}
|
||||
onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')}
|
||||
openCustomizePreferredReactionsModal={action(
|
||||
'openCustomizePreferredReactionsModal'
|
||||
)}
|
||||
preferredReactionEmoji={DEFAULT_PREFERRED_REACTION_EMOJI}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -57,14 +33,7 @@ export function SelectedReaction(): JSX.Element {
|
||||
i18n={i18n}
|
||||
selected={e}
|
||||
onPick={action('onPick')}
|
||||
onEmojiSkinToneDefaultChange={action(
|
||||
'onEmojiSkinToneDefaultChange'
|
||||
)}
|
||||
openCustomizePreferredReactionsModal={action(
|
||||
'openCustomizePreferredReactionsModal'
|
||||
)}
|
||||
preferredReactionEmoji={DEFAULT_PREFERRED_REACTION_EMOJI}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,39 +3,24 @@
|
||||
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
import { convertShortName } from '../emoji/lib.js';
|
||||
import type { Props as EmojiPickerProps } from '../emoji/EmojiPicker.js';
|
||||
import { useDelayedRestoreFocus } from '../../hooks/useRestoreFocus.js';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util.js';
|
||||
import {
|
||||
ReactionPickerPicker,
|
||||
ReactionPickerPickerEmojiButton,
|
||||
ReactionPickerPickerMoreButton,
|
||||
ReactionPickerPickerStyle,
|
||||
} from '../ReactionPickerPicker.js';
|
||||
import type { EmojiSkinTone, EmojiVariantKey } from '../fun/data/emojis.js';
|
||||
import type { EmojiVariantKey } from '../fun/data/emojis.js';
|
||||
import { getEmojiVariantByKey } from '../fun/data/emojis.js';
|
||||
import { FunEmojiPicker } from '../fun/FunEmojiPicker.js';
|
||||
import type { FunEmojiSelection } from '../fun/panels/FunPanelEmojis.js';
|
||||
import { isFunPickerEnabled } from '../fun/isFunPickerEnabled.js';
|
||||
|
||||
export type RenderEmojiPickerProps = Pick<Props, 'onClose' | 'style'> &
|
||||
Pick<
|
||||
EmojiPickerProps,
|
||||
'onClickSettings' | 'onPickEmoji' | 'onEmojiSkinToneDefaultChange'
|
||||
> & {
|
||||
ref: React.Ref<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
i18n: LocalizerType;
|
||||
selected?: string;
|
||||
onClose?: () => unknown;
|
||||
onPick: (emoji: string) => unknown;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => unknown;
|
||||
openCustomizePreferredReactionsModal?: () => unknown;
|
||||
preferredReactionEmoji: ReadonlyArray<string>;
|
||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement;
|
||||
theme?: ThemeType;
|
||||
messageEmojis?: ReadonlyArray<EmojiVariantKey>;
|
||||
};
|
||||
@@ -48,10 +33,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||
i18n,
|
||||
onClose,
|
||||
onPick,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
openCustomizePreferredReactionsModal,
|
||||
preferredReactionEmoji,
|
||||
renderEmojiPicker,
|
||||
selected,
|
||||
style,
|
||||
theme,
|
||||
@@ -80,14 +62,6 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||
setEmojiPickerOpen(open);
|
||||
}, []);
|
||||
|
||||
// Handle EmojiPicker::onPickEmoji
|
||||
const onPickEmoji: EmojiPickerProps['onPickEmoji'] = React.useCallback(
|
||||
({ shortName, skinTone: pickedSkinTone }) => {
|
||||
onPick(convertShortName(shortName, pickedSkinTone));
|
||||
},
|
||||
[onPick]
|
||||
);
|
||||
|
||||
const onSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
const variant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
@@ -99,17 +73,6 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||
// Focus first button and restore focus on unmount
|
||||
const [focusRef] = useDelayedRestoreFocus();
|
||||
|
||||
if (!isFunPickerEnabled() && emojiPickerOpen) {
|
||||
return renderEmojiPicker({
|
||||
onClickSettings: openCustomizePreferredReactionsModal,
|
||||
onClose,
|
||||
onPickEmoji,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
ref,
|
||||
style,
|
||||
});
|
||||
}
|
||||
|
||||
const otherSelected =
|
||||
selected != null && !preferredReactionEmoji.includes(selected);
|
||||
|
||||
@@ -149,32 +112,20 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||
title={i18n('icu:Reactions--remove')}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isFunPickerEnabled() && (
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleFunEmojiPickerOpenChange}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
theme={theme}
|
||||
showCustomizePreferredReactionsButton
|
||||
closeOnSelect
|
||||
messageEmojis={messageEmojis}
|
||||
>
|
||||
<Button
|
||||
aria-label={i18n('icu:Reactions--more')}
|
||||
className="module-ReactionPickerPicker__button module-ReactionPickerPicker__button--more"
|
||||
/>
|
||||
</FunEmojiPicker>
|
||||
)}
|
||||
{!isFunPickerEnabled() && (
|
||||
<ReactionPickerPickerMoreButton
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
setEmojiPickerOpen(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleFunEmojiPickerOpenChange}
|
||||
onSelectEmoji={onSelectEmoji}
|
||||
theme={theme}
|
||||
showCustomizePreferredReactionsButton
|
||||
closeOnSelect
|
||||
messageEmojis={messageEmojis}
|
||||
>
|
||||
<Button
|
||||
aria-label={i18n('icu:Reactions--more')}
|
||||
className="module-ReactionPickerPicker__button module-ReactionPickerPicker__button--more"
|
||||
/>
|
||||
</FunEmojiPicker>
|
||||
)}
|
||||
</ReactionPickerPicker>
|
||||
);
|
||||
|
||||
@@ -10,11 +10,12 @@ import { Avatar } from '../Avatar.js';
|
||||
import { useRestoreFocus } from '../../hooks/useRestoreFocus.js';
|
||||
import type { ConversationType } from '../../state/ducks/conversations.js';
|
||||
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges.js';
|
||||
import type { EmojiData } from '../emoji/lib.js';
|
||||
import { emojiToData } from '../emoji/lib.js';
|
||||
import { useEscapeHandling } from '../../hooks/useEscapeHandling.js';
|
||||
import type { ThemeType } from '../../types/Util.js';
|
||||
import type { EmojiParentKey, EmojiVariantKey } from '../fun/data/emojis.js';
|
||||
import {
|
||||
EMOJI_PARENT_KEY_CONSTANTS,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
@@ -23,7 +24,7 @@ import { strictAssert } from '../../util/assert.js';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.js';
|
||||
import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.js';
|
||||
|
||||
const { groupBy, mapValues, orderBy } = lodash;
|
||||
const { mapValues, orderBy } = lodash;
|
||||
|
||||
export type Reaction = {
|
||||
emoji: string;
|
||||
@@ -56,13 +57,13 @@ export type Props = OwnProps &
|
||||
Pick<AvatarProps, 'i18n'>;
|
||||
|
||||
const DEFAULT_EMOJI_ORDER = [
|
||||
'heart',
|
||||
'+1',
|
||||
'-1',
|
||||
'joy',
|
||||
'open_mouth',
|
||||
'cry',
|
||||
'rage',
|
||||
EMOJI_PARENT_KEY_CONSTANTS.RED_HEART,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.THUMBS_DOWN,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_TEARS_OF_JOY,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_OPEN_MOUTH,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.CRYING_FACE,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.ENRAGED_FACE,
|
||||
];
|
||||
|
||||
type ReactionCategory = {
|
||||
@@ -72,7 +73,11 @@ type ReactionCategory = {
|
||||
index: number;
|
||||
};
|
||||
|
||||
type ReactionWithEmojiData = Reaction & EmojiData;
|
||||
type ReactionWithEmojiData = Reaction &
|
||||
Readonly<{
|
||||
parentKey: EmojiParentKey;
|
||||
variantKey: EmojiVariantKey;
|
||||
}>;
|
||||
|
||||
function ReactionViewerEmoji(props: {
|
||||
emojiVariantValue: string | undefined;
|
||||
@@ -112,37 +117,32 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
|
||||
() =>
|
||||
reactions
|
||||
.map(reaction => {
|
||||
const emojiData = emojiToData(reaction.emoji);
|
||||
|
||||
if (!emojiData) {
|
||||
return undefined;
|
||||
if (!isEmojiVariantValue(reaction.emoji)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...reaction,
|
||||
...emojiData,
|
||||
};
|
||||
const variantKey = getEmojiVariantKeyByValue(reaction.emoji);
|
||||
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
|
||||
return { ...reaction, parentKey, variantKey };
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
reactionWithEmojiData
|
||||
): reactionWithEmojiData is ReactionWithEmojiData =>
|
||||
Boolean(reactionWithEmojiData)
|
||||
),
|
||||
.filter((data): data is ReactionWithEmojiData => {
|
||||
return data != null;
|
||||
}),
|
||||
[reactions]
|
||||
);
|
||||
|
||||
const groupedAndSortedReactions = React.useMemo(
|
||||
() =>
|
||||
mapValues(
|
||||
{
|
||||
all: reactionsWithEmojiData,
|
||||
...groupBy(reactionsWithEmojiData, 'short_name'),
|
||||
},
|
||||
groupedReactions => orderBy(groupedReactions, ['timestamp'], ['desc'])
|
||||
),
|
||||
[reactionsWithEmojiData]
|
||||
);
|
||||
const groupedAndSortedReactions = React.useMemo(() => {
|
||||
const groups = Object.groupBy(reactionsWithEmojiData, data => {
|
||||
return data.parentKey;
|
||||
});
|
||||
|
||||
return mapValues(
|
||||
{
|
||||
all: reactionsWithEmojiData,
|
||||
...groups,
|
||||
},
|
||||
groupedReactions => orderBy(groupedReactions, ['timestamp'], ['desc'])
|
||||
);
|
||||
}, [reactionsWithEmojiData]);
|
||||
|
||||
const reactionCategories: Array<ReactionCategory> = React.useMemo(
|
||||
() =>
|
||||
@@ -159,9 +159,9 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
|
||||
const localUserReaction = groupedReactions.find(r => r.from.isMe);
|
||||
const firstReaction = localUserReaction || groupedReactions[0];
|
||||
return {
|
||||
id: firstReaction.short_name,
|
||||
index: DEFAULT_EMOJI_ORDER.includes(firstReaction.short_name)
|
||||
? DEFAULT_EMOJI_ORDER.indexOf(firstReaction.short_name)
|
||||
id: firstReaction.parentKey,
|
||||
index: DEFAULT_EMOJI_ORDER.includes(firstReaction.parentKey)
|
||||
? DEFAULT_EMOJI_ORDER.indexOf(firstReaction.parentKey)
|
||||
: Infinity,
|
||||
emoji: firstReaction.emoji,
|
||||
count: groupedReactions.length,
|
||||
|
||||
@@ -376,7 +376,6 @@ const renderItem = ({
|
||||
item={items[messageId]}
|
||||
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
|
||||
renderContact={() => <div>*ContactName*</div>}
|
||||
renderEmojiPicker={() => <div />}
|
||||
renderReactionPicker={() => <div />}
|
||||
renderUniversalTimerNotification={() => (
|
||||
<div>*UniversalTimerNotification*</div>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { EmojiPicker } from '../emoji/EmojiPicker.js';
|
||||
import { DurationInSeconds } from '../../util/durations/index.js';
|
||||
import type { PropsType as TimelineItemProps } from './TimelineItem.js';
|
||||
import { TimelineItem } from './TimelineItem.js';
|
||||
@@ -16,28 +15,9 @@ import { WidthBreakpoint } from '../_util.js';
|
||||
import { ThemeType } from '../../types/Util.js';
|
||||
import { PaymentEventKind } from '../../types/Payment.js';
|
||||
import { ErrorBoundary } from './ErrorBoundary.js';
|
||||
import { EmojiSkinTone } from '../fun/data/emojis.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
const renderEmojiPicker: TimelineItemProps['renderEmojiPicker'] = ({
|
||||
onClose,
|
||||
onPickEmoji,
|
||||
ref,
|
||||
}) => (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
onEmojiSkinToneDefaultChange={action(
|
||||
'EmojiPicker::onEmojiSkinToneDefaultChange'
|
||||
)}
|
||||
ref={ref}
|
||||
onClose={onClose}
|
||||
onPickEmoji={onPickEmoji}
|
||||
wasInvokedFromKeyboard={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderReactionPicker: TimelineItemProps['renderReactionPicker'] = () => (
|
||||
<div />
|
||||
);
|
||||
@@ -124,7 +104,6 @@ const getDefaultProps = () => ({
|
||||
|
||||
renderContact,
|
||||
renderUniversalTimerNotification,
|
||||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||
viewStory: action('viewStory'),
|
||||
|
||||
@@ -210,7 +210,6 @@ export type PropsType = PropsLocalType &
|
||||
AllMessageProps,
|
||||
| 'containerWidthBreakpoint'
|
||||
| 'getPreferredBadge'
|
||||
| 'renderEmojiPicker'
|
||||
| 'renderAudioAttachment'
|
||||
| 'renderReactionPicker'
|
||||
| 'shouldCollapseAbove'
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { SignalService } from '../../protobuf/index.js';
|
||||
import { ConversationColors } from '../../types/Colors.js';
|
||||
import { EmojiPicker } from '../emoji/EmojiPicker.js';
|
||||
import type { AudioAttachmentProps } from './Message.js';
|
||||
import type { Props } from './TimelineMessage.js';
|
||||
import { TimelineMessage } from './TimelineMessage.js';
|
||||
@@ -44,7 +43,6 @@ import { getFakeBadge } from '../../test-helpers/getFakeBadge.js';
|
||||
import { ThemeType } from '../../types/Util.js';
|
||||
import { BadgeCategory } from '../../badges/BadgeCategory.js';
|
||||
import { PaymentEventKind } from '../../types/Payment.js';
|
||||
import { EmojiSkinTone } from '../fun/data/emojis.js';
|
||||
|
||||
const { isBoolean, noop } = lodash;
|
||||
|
||||
@@ -110,24 +108,6 @@ function getJoyReaction() {
|
||||
};
|
||||
}
|
||||
|
||||
const renderEmojiPicker: Props['renderEmojiPicker'] = ({
|
||||
onClose,
|
||||
onPickEmoji,
|
||||
ref,
|
||||
}) => (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
onEmojiSkinToneDefaultChange={action(
|
||||
'EmojiPicker::onEmojiSkinToneDefaultChange'
|
||||
)}
|
||||
ref={ref}
|
||||
onClose={onClose}
|
||||
onPickEmoji={onPickEmoji}
|
||||
wasInvokedFromKeyboard={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderReactionPicker: Props['renderReactionPicker'] = () => <div />;
|
||||
|
||||
/**
|
||||
@@ -319,7 +299,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
overrideProps.readStatus === undefined
|
||||
? ReadStatus.Read
|
||||
: overrideProps.readStatus,
|
||||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
renderAudioAttachment,
|
||||
saveAttachment: action('saveAttachment'),
|
||||
|
||||
@@ -31,7 +31,6 @@ import type {
|
||||
} from './Message.js';
|
||||
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations.js';
|
||||
import { doesMessageBodyOverflow } from './MessageBodyReadMore.js';
|
||||
import type { Props as ReactionPickerProps } from './ReactionPicker.js';
|
||||
import {
|
||||
useKeyboardShortcutsConditionally,
|
||||
useOpenContextMenu,
|
||||
@@ -90,8 +89,7 @@ export type PropsActions = {
|
||||
|
||||
export type Props = PropsData &
|
||||
PropsActions &
|
||||
Omit<PropsHousekeeping, 'isAttachmentPending'> &
|
||||
Pick<ReactionPickerProps, 'renderEmojiPicker'> & {
|
||||
Omit<PropsHousekeeping, 'isAttachmentPending'> & {
|
||||
renderReactionPicker: (
|
||||
props: React.ComponentProps<typeof SmartReactionPicker>
|
||||
) => JSX.Element;
|
||||
@@ -123,7 +121,6 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||
copyMessageText,
|
||||
pushPanelForConversation,
|
||||
reactToMessage,
|
||||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
retryDeleteForEveryone,
|
||||
retryMessageSend,
|
||||
@@ -343,7 +340,6 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||
remove: emoji === selectedReaction,
|
||||
});
|
||||
},
|
||||
renderEmojiPicker,
|
||||
messageEmojis,
|
||||
})
|
||||
}
|
||||
@@ -369,7 +365,6 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||
renderReactionPicker,
|
||||
selectedReaction,
|
||||
reactToMessage,
|
||||
renderEmojiPicker,
|
||||
toggleReactionPicker,
|
||||
id,
|
||||
messageEmojis,
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { Props } from './EmojiButton.js';
|
||||
import { EmojiButton } from './EmojiButton.js';
|
||||
import { EmojiSkinTone } from '../fun/data/emojis.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'Components/Emoji/EmojiButton',
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
export function Base(): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<EmojiButton
|
||||
i18n={i18n}
|
||||
onPickEmoji={action('onPickEmoji')}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')}
|
||||
recentEmojis={[
|
||||
'grinning',
|
||||
'grin',
|
||||
'joy',
|
||||
'rolling_on_the_floor_laughing',
|
||||
'smiley',
|
||||
'smile',
|
||||
'sweat_smile',
|
||||
'laughing',
|
||||
'wink',
|
||||
'blush',
|
||||
'yum',
|
||||
'sunglasses',
|
||||
'heart_eyes',
|
||||
'kissing_heart',
|
||||
'kissing',
|
||||
'kissing_smiling_eyes',
|
||||
'kissing_closed_eyes',
|
||||
'relaxed',
|
||||
'slightly_smiling_face',
|
||||
'hugging_face',
|
||||
'grinning_face_with_star_eyes',
|
||||
'thinking_face',
|
||||
'face_with_one_eyebrow_raised',
|
||||
'neutral_face',
|
||||
'expressionless',
|
||||
'no_mouth',
|
||||
'face_with_rolling_eyes',
|
||||
'smirk',
|
||||
'persevere',
|
||||
'disappointed_relieved',
|
||||
'open_mouth',
|
||||
'zipper_mouth_face',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import lodash from 'lodash';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import type { Props as EmojiPickerProps } from './EmojiPicker.js';
|
||||
import { EmojiPicker } from './EmojiPicker.js';
|
||||
import type { LocalizerType } from '../../types/Util.js';
|
||||
import { useRefMerger } from '../../hooks/useRefMerger.js';
|
||||
import { handleOutsideClick } from '../../util/handleOutsideClick.js';
|
||||
import * as KeyboardLayout from '../../services/keyboardLayout.js';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.js';
|
||||
import type { EmojiVariantData } from '../fun/data/emojis.js';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../fun/data/emojis.js';
|
||||
|
||||
const { get, noop } = lodash;
|
||||
|
||||
export enum EmojiButtonVariant {
|
||||
Normal,
|
||||
ProfileEditor,
|
||||
}
|
||||
|
||||
export type OwnProps = Readonly<{
|
||||
className?: string;
|
||||
closeOnPick?: boolean;
|
||||
emoji?: string;
|
||||
i18n: LocalizerType;
|
||||
onClose?: () => unknown;
|
||||
onOpen?: () => unknown;
|
||||
emojiButtonApi?: MutableRefObject<EmojiButtonAPI | undefined>;
|
||||
variant?: EmojiButtonVariant;
|
||||
}>;
|
||||
|
||||
export type Props = OwnProps &
|
||||
Pick<
|
||||
EmojiPickerProps,
|
||||
| 'onPickEmoji'
|
||||
| 'onEmojiSkinToneDefaultChange'
|
||||
| 'recentEmojis'
|
||||
| 'emojiSkinToneDefault'
|
||||
>;
|
||||
|
||||
export type EmojiButtonAPI = Readonly<{
|
||||
close: () => void;
|
||||
}>;
|
||||
|
||||
export const EmojiButton = React.memo(function EmojiButtonInner({
|
||||
className,
|
||||
closeOnPick,
|
||||
emoji,
|
||||
emojiButtonApi,
|
||||
i18n,
|
||||
onClose,
|
||||
onOpen,
|
||||
onPickEmoji,
|
||||
emojiSkinToneDefault,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
recentEmojis,
|
||||
variant = EmojiButtonVariant.Normal,
|
||||
}: Props) {
|
||||
const isRTL = i18n.getLocaleDirection() === 'rtl';
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [wasInvokedFromKeyboard, setWasInvokedFromKeyboard] =
|
||||
React.useState(false);
|
||||
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const popperRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const refMerger = useRefMerger();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
onOpen?.();
|
||||
}, [open, onOpen]);
|
||||
|
||||
const handleClickButton = React.useCallback(() => {
|
||||
setWasInvokedFromKeyboard(false);
|
||||
if (open) {
|
||||
setOpen(false);
|
||||
} else {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [open, setOpen, setWasInvokedFromKeyboard]);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setOpen(false);
|
||||
setWasInvokedFromKeyboard(false);
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}, [setOpen, setWasInvokedFromKeyboard, onClose]);
|
||||
|
||||
const api = React.useMemo(
|
||||
() => ({
|
||||
close: () => {
|
||||
setOpen(false);
|
||||
setWasInvokedFromKeyboard(false);
|
||||
},
|
||||
}),
|
||||
[setOpen, setWasInvokedFromKeyboard]
|
||||
);
|
||||
|
||||
if (emojiButtonApi) {
|
||||
// Using a React.MutableRefObject, so we need to reassign this prop.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
emojiButtonApi.current = api;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
return handleOutsideClick(
|
||||
() => {
|
||||
handleClose();
|
||||
return true;
|
||||
},
|
||||
{
|
||||
containerElements: [popperRef, buttonRef],
|
||||
name: 'EmojiButton',
|
||||
}
|
||||
);
|
||||
}, [open, handleClose]);
|
||||
|
||||
// Install keyboard shortcut to open emoji picker
|
||||
React.useEffect(() => {
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
const { ctrlKey, metaKey, shiftKey } = event;
|
||||
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
|
||||
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
|
||||
const commandOrCtrl = commandKey || controlKey;
|
||||
const key = KeyboardLayout.lookup(event);
|
||||
|
||||
// We don't want to open up if the current conversation panel is hidden
|
||||
const parentPanel = buttonRef.current?.closest('.ConversationPanel');
|
||||
if (parentPanel?.classList.contains('ConversationPanel__hidden')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandOrCtrl && shiftKey && (key === 'j' || key === 'J')) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
setWasInvokedFromKeyboard(true);
|
||||
setOpen(!open);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
}, [open, setOpen]);
|
||||
|
||||
let emojiVariant: EmojiVariantData;
|
||||
if (emoji != null && isEmojiVariantValue(emoji)) {
|
||||
const emojiVariantKey = getEmojiVariantKeyByValue(emoji);
|
||||
emojiVariant = getEmojiVariantByKey(emojiVariantKey);
|
||||
}
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<button
|
||||
type="button"
|
||||
ref={refMerger(buttonRef, ref)}
|
||||
onClick={handleClickButton}
|
||||
className={classNames(className, {
|
||||
'module-emoji-button__button': true,
|
||||
'module-emoji-button__button--active': open,
|
||||
'module-emoji-button__button--has-emoji': Boolean(emoji),
|
||||
'module-emoji-button__button--profile-editor':
|
||||
variant === EmojiButtonVariant.ProfileEditor,
|
||||
})}
|
||||
aria-label={i18n('icu:EmojiButton__label')}
|
||||
>
|
||||
{emojiVariant && (
|
||||
<FunStaticEmoji
|
||||
role="presentation"
|
||||
emoji={emojiVariant}
|
||||
size={24}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</Reference>
|
||||
{open ? (
|
||||
<div ref={popperRef}>
|
||||
<Popper placement={isRTL ? 'top-end' : 'top-start'} strategy="fixed">
|
||||
{({ ref, style }) => (
|
||||
<EmojiPicker
|
||||
ref={ref}
|
||||
i18n={i18n}
|
||||
style={style}
|
||||
onPickEmoji={ev => {
|
||||
onPickEmoji(ev);
|
||||
if (closeOnPick) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
onClose={handleClose}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
wasInvokedFromKeyboard={wasInvokedFromKeyboard}
|
||||
recentEmojis={recentEmojis}
|
||||
/>
|
||||
)}
|
||||
</Popper>
|
||||
</div>
|
||||
) : null}
|
||||
</Manager>
|
||||
);
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { Props } from './EmojiPicker.js';
|
||||
import { EmojiPicker } from './EmojiPicker.js';
|
||||
import { EmojiSkinTone } from '../fun/data/emojis.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'Components/Emoji/EmojiPicker',
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
export function Base(): JSX.Element {
|
||||
return (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
onPickEmoji={action('onPickEmoji')}
|
||||
onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')}
|
||||
onClose={action('onClose')}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
recentEmojis={[
|
||||
'grinning',
|
||||
'grin',
|
||||
'joy',
|
||||
'rolling_on_the_floor_laughing',
|
||||
'smiley',
|
||||
'smile',
|
||||
'sweat_smile',
|
||||
'laughing',
|
||||
'wink',
|
||||
'blush',
|
||||
'yum',
|
||||
'sunglasses',
|
||||
'heart_eyes',
|
||||
'kissing_heart',
|
||||
'kissing',
|
||||
'kissing_smiling_eyes',
|
||||
'kissing_closed_eyes',
|
||||
'relaxed',
|
||||
'slightly_smiling_face',
|
||||
'hugging_face',
|
||||
'grinning_face_with_star_eyes',
|
||||
'thinking_face',
|
||||
'face_with_one_eyebrow_raised',
|
||||
'neutral_face',
|
||||
'expressionless',
|
||||
'no_mouth',
|
||||
'face_with_rolling_eyes',
|
||||
'smirk',
|
||||
'persevere',
|
||||
'disappointed_relieved',
|
||||
'open_mouth',
|
||||
'zipper_mouth_face',
|
||||
]}
|
||||
wasInvokedFromKeyboard={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoRecents(): JSX.Element {
|
||||
return (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
onPickEmoji={action('onPickEmoji')}
|
||||
onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')}
|
||||
onClose={action('onClose')}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
recentEmojis={[]}
|
||||
wasInvokedFromKeyboard={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function WithSettingsButton(): JSX.Element {
|
||||
return (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
onPickEmoji={action('onPickEmoji')}
|
||||
onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')}
|
||||
onClickSettings={action('onClickSettings')}
|
||||
onClose={action('onClose')}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
recentEmojis={[]}
|
||||
wasInvokedFromKeyboard={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,618 +0,0 @@
|
||||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type {
|
||||
GridCellRenderer,
|
||||
SectionRenderedParams,
|
||||
} from 'react-virtualized';
|
||||
import { AutoSizer, Grid } from 'react-virtualized';
|
||||
import lodash from 'lodash';
|
||||
import { FocusScope } from 'react-aria';
|
||||
import { dataByCategory } from './lib.js';
|
||||
import type { LocalizerType } from '../../types/Util.js';
|
||||
import { isSingleGrapheme } from '../../util/grapheme.js';
|
||||
import { missingCaseError } from '../../util/missingCaseError.js';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.js';
|
||||
import { strictAssert } from '../../util/assert.js';
|
||||
import {
|
||||
EMOJI_SKIN_TONE_ORDER,
|
||||
emojiParentKeyConstant,
|
||||
EmojiSkinTone,
|
||||
emojiVariantConstant,
|
||||
getEmojiParentKeyByEnglishShortName,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
isEmojiEnglishShortName,
|
||||
EMOJI_SKIN_TONE_TO_NUMBER,
|
||||
getEmojiParentByKey,
|
||||
} from '../fun/data/emojis.js';
|
||||
import { useFunEmojiSearch } from '../fun/useFunEmojiSearch.js';
|
||||
|
||||
const { chunk, clamp, debounce, findLast, flatMap, initial, last, zipObject } =
|
||||
lodash;
|
||||
|
||||
export type EmojiPickDataType = {
|
||||
skinTone: EmojiSkinTone;
|
||||
shortName: string;
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly recentEmojis?: ReadonlyArray<string>;
|
||||
readonly emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
readonly onClickSettings?: () => unknown;
|
||||
readonly onClose?: () => unknown;
|
||||
readonly onPickEmoji: (o: EmojiPickDataType) => unknown;
|
||||
readonly onEmojiSkinToneDefaultChange?: (
|
||||
emojiSkinTone: EmojiSkinTone
|
||||
) => void;
|
||||
readonly wasInvokedFromKeyboard: boolean;
|
||||
};
|
||||
|
||||
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||
|
||||
function isEventFromMouse(
|
||||
event:
|
||||
| React.MouseEvent<HTMLButtonElement>
|
||||
| React.KeyboardEvent<HTMLButtonElement>
|
||||
): boolean {
|
||||
return (
|
||||
('clientX' in event && event.clientX !== 0) ||
|
||||
('clientY' in event && event.clientY !== 0)
|
||||
);
|
||||
}
|
||||
|
||||
function focusOnRender(el: HTMLElement | null) {
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const COL_COUNT = 8;
|
||||
|
||||
const categories = [
|
||||
'recents',
|
||||
'emoji',
|
||||
'animal',
|
||||
'food',
|
||||
'activity',
|
||||
'travel',
|
||||
'object',
|
||||
'symbol',
|
||||
'flag',
|
||||
] as const;
|
||||
|
||||
type Category = (typeof categories)[number];
|
||||
|
||||
export const EmojiPicker = React.memo(
|
||||
React.forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{
|
||||
i18n,
|
||||
onPickEmoji,
|
||||
emojiSkinToneDefault,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
recentEmojis = [],
|
||||
style,
|
||||
onClickSettings,
|
||||
onClose,
|
||||
wasInvokedFromKeyboard,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const isRTL = i18n.getLocaleDirection() === 'rtl';
|
||||
|
||||
const [isUsingKeyboard, setIsUsingKeyboard] = React.useState(
|
||||
wasInvokedFromKeyboard
|
||||
);
|
||||
|
||||
const [firstRecent] = React.useState(recentEmojis);
|
||||
const [selectedCategory, setSelectedCategory] = React.useState<Category>(
|
||||
categories[0]
|
||||
);
|
||||
const [searchMode, setSearchMode] = React.useState(false);
|
||||
const [searchText, setSearchText] = React.useState('');
|
||||
const [scrollToRow, setScrollToRow] = React.useState(0);
|
||||
const [selectedTone, setSelectedTone] =
|
||||
React.useState(emojiSkinToneDefault);
|
||||
|
||||
const emojiSearch = useFunEmojiSearch();
|
||||
|
||||
const handleToggleSearch = React.useCallback(
|
||||
(
|
||||
e:
|
||||
| React.MouseEvent<HTMLButtonElement>
|
||||
| React.KeyboardEvent<HTMLButtonElement>
|
||||
) => {
|
||||
if (isEventFromMouse(e)) {
|
||||
setIsUsingKeyboard(false);
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
setSearchText('');
|
||||
setSelectedCategory(categories[0]);
|
||||
setSearchMode(m => !m);
|
||||
},
|
||||
[setSearchText, setSearchMode]
|
||||
);
|
||||
|
||||
const debounceSearchChange = React.useMemo(
|
||||
() =>
|
||||
debounce((query: string) => {
|
||||
setScrollToRow(0);
|
||||
setSearchText(query);
|
||||
}, 200),
|
||||
[setSearchText, setScrollToRow]
|
||||
);
|
||||
|
||||
const handleSearchChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
debounceSearchChange(e.currentTarget.value);
|
||||
},
|
||||
[debounceSearchChange]
|
||||
);
|
||||
|
||||
const handlePickEmoji = React.useCallback(
|
||||
(
|
||||
e:
|
||||
| React.MouseEvent<HTMLButtonElement>
|
||||
| React.KeyboardEvent<HTMLButtonElement>
|
||||
) => {
|
||||
const { shortName } = e.currentTarget.dataset;
|
||||
if ('key' in e) {
|
||||
if (e.key === 'Enter') {
|
||||
if (shortName && isUsingKeyboard) {
|
||||
onPickEmoji({
|
||||
skinTone: selectedTone ?? EmojiSkinTone.None,
|
||||
shortName,
|
||||
});
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else if (onClose) {
|
||||
onClose();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
} else if (shortName) {
|
||||
if (isEventFromMouse(e)) {
|
||||
setIsUsingKeyboard(false);
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onPickEmoji({
|
||||
skinTone: selectedTone ?? EmojiSkinTone.None,
|
||||
shortName,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
onClose,
|
||||
onPickEmoji,
|
||||
isUsingKeyboard,
|
||||
selectedTone,
|
||||
setIsUsingKeyboard,
|
||||
]
|
||||
);
|
||||
|
||||
// Handle key presses, particularly Escape
|
||||
React.useEffect(() => {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Tab') {
|
||||
// We do NOT prevent default here to allow Tab to be used normally
|
||||
setIsUsingKeyboard(true);
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
if (searchMode) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setScrollToRow(0);
|
||||
setSearchText('');
|
||||
setSearchMode(false);
|
||||
} else if (onClose) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
} else if (!searchMode && !event.ctrlKey && !event.metaKey) {
|
||||
if (
|
||||
[
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'Enter',
|
||||
'Shift',
|
||||
' ', // Space
|
||||
].includes(event.key)
|
||||
) {
|
||||
// Do nothing, these can be used to navigate around the picker.
|
||||
} else if (isSingleGrapheme(event.key)) {
|
||||
// A single grapheme means the user is typing text. Switch to search mode.
|
||||
setSelectedCategory(categories[0]);
|
||||
setSearchMode(true);
|
||||
// Continue propagation, typing the first letter for search.
|
||||
} else {
|
||||
// For anything else, assume it's a special key that isn't one of the ones
|
||||
// above (such as Delete or ContextMenu).
|
||||
onClose?.();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handler);
|
||||
};
|
||||
}, [onClose, setIsUsingKeyboard, searchMode, setSearchMode]);
|
||||
|
||||
const [, ...renderableCategories] = categories;
|
||||
|
||||
const emojiGrid = React.useMemo(() => {
|
||||
if (searchText) {
|
||||
return chunk(
|
||||
emojiSearch(searchText).map(result => {
|
||||
const parent = getEmojiParentByKey(result.parentKey);
|
||||
return parent.englishShortNameDefault;
|
||||
}),
|
||||
COL_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
const chunks = flatMap(renderableCategories, cat =>
|
||||
chunk(
|
||||
dataByCategory[cat].map(e => e.short_name),
|
||||
COL_COUNT
|
||||
)
|
||||
);
|
||||
|
||||
return [...chunk(firstRecent, COL_COUNT), ...chunks];
|
||||
}, [firstRecent, renderableCategories, searchText, emojiSearch]);
|
||||
|
||||
const rowCount = emojiGrid.length;
|
||||
|
||||
const catRowEnds = React.useMemo(() => {
|
||||
const rowEnds: Array<number> = [
|
||||
Math.ceil(firstRecent.length / COL_COUNT) - 1,
|
||||
];
|
||||
|
||||
renderableCategories.forEach(cat => {
|
||||
rowEnds.push(
|
||||
Math.ceil(dataByCategory[cat].length / COL_COUNT) +
|
||||
(last(rowEnds) as number)
|
||||
);
|
||||
});
|
||||
|
||||
return rowEnds;
|
||||
}, [firstRecent.length, renderableCategories]);
|
||||
|
||||
const catToRowOffsets = React.useMemo(() => {
|
||||
const offsets = initial(catRowEnds).map(i => i + 1);
|
||||
|
||||
return zipObject(categories, [0, ...offsets]);
|
||||
}, [catRowEnds]);
|
||||
|
||||
const catOffsetEntries = React.useMemo(
|
||||
() => Object.entries(catToRowOffsets),
|
||||
[catToRowOffsets]
|
||||
);
|
||||
|
||||
const handleSelectCategory = React.useCallback(
|
||||
(
|
||||
e:
|
||||
| React.MouseEvent<HTMLButtonElement>
|
||||
| React.KeyboardEvent<HTMLButtonElement>
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const { category } = e.currentTarget.dataset;
|
||||
if (category) {
|
||||
setSelectedCategory(category as Category);
|
||||
setScrollToRow(catToRowOffsets[category]);
|
||||
}
|
||||
},
|
||||
[catToRowOffsets, setSelectedCategory, setScrollToRow]
|
||||
);
|
||||
|
||||
const cellRenderer = React.useCallback<GridCellRenderer>(
|
||||
({ key, style: cellStyle, rowIndex, columnIndex }) => {
|
||||
const shortName = emojiGrid[rowIndex][columnIndex];
|
||||
|
||||
if (!shortName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
strictAssert(
|
||||
isEmojiEnglishShortName(shortName),
|
||||
'Must be a valid emoji short name'
|
||||
);
|
||||
const parentKey = getEmojiParentKeyByEnglishShortName(shortName);
|
||||
const variantKey = getEmojiVariantByParentKeyAndSkinTone(
|
||||
parentKey,
|
||||
selectedTone ?? EmojiSkinTone.None
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="module-emoji-picker__body__emoji-cell"
|
||||
style={cellStyle}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="module-emoji-picker__button"
|
||||
onClick={handlePickEmoji}
|
||||
onKeyDown={handlePickEmoji}
|
||||
data-short-name={shortName}
|
||||
title={shortName}
|
||||
>
|
||||
<FunStaticEmoji
|
||||
role="presentation"
|
||||
emoji={variantKey}
|
||||
size={28}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[emojiGrid, handlePickEmoji, selectedTone]
|
||||
);
|
||||
|
||||
const getRowHeight = React.useCallback(
|
||||
({ index }: { index: number }) => {
|
||||
if (searchText) {
|
||||
return 34;
|
||||
}
|
||||
|
||||
if (catRowEnds.includes(index) && index !== last(catRowEnds)) {
|
||||
return 44;
|
||||
}
|
||||
|
||||
return 34;
|
||||
},
|
||||
[catRowEnds, searchText]
|
||||
);
|
||||
|
||||
const onSectionRendered = React.useMemo(
|
||||
() =>
|
||||
debounce(({ rowStartIndex }: SectionRenderedParams) => {
|
||||
const [cat] =
|
||||
findLast(catOffsetEntries, ([, row]) => rowStartIndex >= row) ||
|
||||
categories;
|
||||
|
||||
setSelectedCategory(cat as Category);
|
||||
}, 10),
|
||||
[catOffsetEntries]
|
||||
);
|
||||
|
||||
function getCategoryButtonLabel(category: Category): string {
|
||||
switch (category) {
|
||||
case 'recents':
|
||||
return i18n('icu:EmojiPicker__button--recents');
|
||||
case 'emoji':
|
||||
return i18n('icu:EmojiPicker__button--emoji');
|
||||
case 'animal':
|
||||
return i18n('icu:EmojiPicker__button--animal');
|
||||
case 'food':
|
||||
return i18n('icu:EmojiPicker__button--food');
|
||||
case 'activity':
|
||||
return i18n('icu:EmojiPicker__button--activity');
|
||||
case 'travel':
|
||||
return i18n('icu:EmojiPicker__button--travel');
|
||||
case 'object':
|
||||
return i18n('icu:EmojiPicker__button--object');
|
||||
case 'symbol':
|
||||
return i18n('icu:EmojiPicker__button--symbol');
|
||||
case 'flag':
|
||||
return i18n('icu:EmojiPicker__button--flag');
|
||||
default:
|
||||
throw missingCaseError(category);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FocusScope contain>
|
||||
<div className="module-emoji-picker" ref={ref} style={style}>
|
||||
<header className="module-emoji-picker__header">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleSearch}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'Enter' || event.key === 'Select') {
|
||||
handleToggleSearch(event);
|
||||
}
|
||||
}}
|
||||
title={
|
||||
searchMode
|
||||
? i18n('icu:EmojiPicker--search-close')
|
||||
: i18n('icu:EmojiPicker--search-placeholder')
|
||||
}
|
||||
className={classNames(
|
||||
'module-emoji-picker__button',
|
||||
'module-emoji-picker__button--icon',
|
||||
searchMode
|
||||
? 'module-emoji-picker__button--icon--close'
|
||||
: 'module-emoji-picker__button--icon--search'
|
||||
)}
|
||||
aria-label={i18n('icu:EmojiPicker--search-placeholder')}
|
||||
/>
|
||||
{searchMode ? (
|
||||
<div className="module-emoji-picker__header__search-field">
|
||||
<input
|
||||
ref={focusOnRender}
|
||||
className="module-emoji-picker__header__search-field__input"
|
||||
placeholder={i18n('icu:EmojiPicker--search-placeholder')}
|
||||
onChange={handleSearchChange}
|
||||
dir="auto"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
categories.map(cat =>
|
||||
cat === 'recents' && firstRecent.length === 0 ? null : (
|
||||
<button
|
||||
aria-pressed={selectedCategory === cat}
|
||||
type="button"
|
||||
key={cat}
|
||||
data-category={cat}
|
||||
title={cat}
|
||||
onClick={handleSelectCategory}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'Enter' || event.key === 'Space') {
|
||||
handleSelectCategory(event);
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
'module-emoji-picker__button',
|
||||
'module-emoji-picker__button--icon',
|
||||
`module-emoji-picker__button--icon--${cat}`,
|
||||
selectedCategory === cat
|
||||
? 'module-emoji-picker__button--selected'
|
||||
: null
|
||||
)}
|
||||
aria-label={getCategoryButtonLabel(cat)}
|
||||
/>
|
||||
)
|
||||
)
|
||||
)}
|
||||
</header>
|
||||
{rowCount > 0 ? (
|
||||
<div>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<Grid
|
||||
key={searchText}
|
||||
className="module-emoji-picker__body"
|
||||
width={width}
|
||||
height={height}
|
||||
columnCount={COL_COUNT}
|
||||
columnWidth={38}
|
||||
// react-virtualized Grid default style has direction: 'ltr'
|
||||
style={{ direction: isRTL ? 'rtl' : 'ltr' }}
|
||||
rowHeight={getRowHeight}
|
||||
rowCount={rowCount}
|
||||
cellRenderer={cellRenderer}
|
||||
// In some cases, `scrollToRow` can be too high for a short period
|
||||
// during state changes. This ensures that the value is never too
|
||||
// large.
|
||||
scrollToRow={clamp(scrollToRow, 0, rowCount - 1)}
|
||||
scrollToAlignment="start"
|
||||
onSectionRendered={onSectionRendered}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-emoji-picker__body',
|
||||
'module-emoji-picker__body--empty'
|
||||
)}
|
||||
>
|
||||
{i18n('icu:EmojiPicker--empty')}
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
marginInlineStart: '4px',
|
||||
}}
|
||||
>
|
||||
<FunStaticEmoji
|
||||
role="presentation"
|
||||
// Slightly Frowning Face
|
||||
emoji={emojiVariantConstant('\u{1F641}')}
|
||||
size={16}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<footer className="module-emoji-picker__footer">
|
||||
{Boolean(onClickSettings) && (
|
||||
<button
|
||||
aria-label={i18n('icu:CustomizingPreferredReactions__title')}
|
||||
className="module-emoji-picker__button module-emoji-picker__button--footer module-emoji-picker__button--settings"
|
||||
onClick={event => {
|
||||
if (onClickSettings) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClickSettings();
|
||||
}
|
||||
}}
|
||||
onKeyDown={event => {
|
||||
if (
|
||||
onClickSettings &&
|
||||
(event.key === 'Enter' || event.key === 'Space')
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClickSettings();
|
||||
}
|
||||
}}
|
||||
title={i18n('icu:CustomizingPreferredReactions__title')}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
{onEmojiSkinToneDefaultChange != null ? (
|
||||
<div className="module-emoji-picker__footer__skin-tones">
|
||||
{EMOJI_SKIN_TONE_ORDER.map(emojiSkinTone => {
|
||||
return (
|
||||
<button
|
||||
aria-pressed={selectedTone === emojiSkinTone}
|
||||
type="button"
|
||||
key={emojiSkinTone}
|
||||
data-tone={emojiSkinTone}
|
||||
onClick={() => {
|
||||
setIsUsingKeyboard(false);
|
||||
setSelectedTone(emojiSkinTone);
|
||||
onEmojiSkinToneDefaultChange(emojiSkinTone);
|
||||
}}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'Enter' || event.key === 'Space') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setSelectedTone(emojiSkinTone);
|
||||
onEmojiSkinToneDefaultChange(emojiSkinTone);
|
||||
}
|
||||
}}
|
||||
title={i18n('icu:EmojiPicker--skin-tone', {
|
||||
tone: `${EMOJI_SKIN_TONE_TO_NUMBER.get(emojiSkinTone)}`,
|
||||
})}
|
||||
className={classNames(
|
||||
'module-emoji-picker__button',
|
||||
'module-emoji-picker__button--footer',
|
||||
selectedTone === emojiSkinTone
|
||||
? 'module-emoji-picker__button--selected'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
<FunStaticEmoji
|
||||
role="presentation"
|
||||
// Raised Hand
|
||||
emoji={getEmojiVariantByParentKeyAndSkinTone(
|
||||
emojiParentKeyConstant('\u{270B}'),
|
||||
emojiSkinTone
|
||||
)}
|
||||
size={20}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{Boolean(onClickSettings) && (
|
||||
<div className="module-emoji-picker__footer__settings-spacer" />
|
||||
)}
|
||||
</footer>
|
||||
</div>
|
||||
</FocusScope>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -1,219 +0,0 @@
|
||||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Camelcase disabled due to emoji-datasource using snake_case
|
||||
/* eslint-disable camelcase */
|
||||
import lodash from 'lodash';
|
||||
import { getOwn } from '../../util/getOwn.js';
|
||||
import {
|
||||
EMOJI_SKIN_TONE_TO_KEY,
|
||||
EmojiSkinTone,
|
||||
KEY_TO_EMOJI_SKIN_TONE,
|
||||
} from '../fun/data/emojis.js';
|
||||
import { strictAssert } from '../../util/assert.js';
|
||||
|
||||
const { groupBy, keyBy, mapValues, sortBy } = lodash;
|
||||
|
||||
// Import emoji-datasource dynamically to avoid costly typechecking.
|
||||
// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires
|
||||
const untypedData = require('emoji-datasource' as string);
|
||||
|
||||
export const skinTones = ['1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'];
|
||||
|
||||
export type SkinToneKey = '1F3FB' | '1F3FC' | '1F3FD' | '1F3FE' | '1F3FF';
|
||||
|
||||
type EmojiSkinVariation = {
|
||||
unified: string;
|
||||
non_qualified: null;
|
||||
image: string;
|
||||
sheet_x: number;
|
||||
sheet_y: number;
|
||||
added_in: string;
|
||||
has_img_apple: boolean;
|
||||
has_img_google: boolean;
|
||||
has_img_twitter: boolean;
|
||||
has_img_emojione: boolean;
|
||||
has_img_facebook: boolean;
|
||||
has_img_messenger: boolean;
|
||||
};
|
||||
|
||||
export type EmojiData = {
|
||||
name: string;
|
||||
unified: string;
|
||||
non_qualified: string | null;
|
||||
docomo: string | null;
|
||||
au: string | null;
|
||||
softbank: string | null;
|
||||
google: string | null;
|
||||
image: string;
|
||||
sheet_x: number;
|
||||
sheet_y: number;
|
||||
short_name: string;
|
||||
short_names: Array<string>;
|
||||
text: string | null;
|
||||
texts: Array<string> | null;
|
||||
category: string;
|
||||
sort_order: number;
|
||||
added_in: string;
|
||||
has_img_apple: boolean;
|
||||
has_img_google: boolean;
|
||||
has_img_twitter: boolean;
|
||||
has_img_facebook: boolean;
|
||||
skin_variations?: {
|
||||
[key: string]: EmojiSkinVariation;
|
||||
};
|
||||
};
|
||||
|
||||
export const data = (untypedData as Array<EmojiData>)
|
||||
.filter(emoji => emoji.has_img_apple)
|
||||
.map(emoji =>
|
||||
// Why this weird map?
|
||||
// the emoji dataset has two separate categories for Emotions and People
|
||||
// yet in our UI we display these as a single merged category. In order
|
||||
// for the emojis to be sorted properly we're manually incrementing the
|
||||
// sort_order for the People & Body emojis so that they fall below the
|
||||
// Smiley & Emotions category.
|
||||
emoji.category === 'People & Body'
|
||||
? { ...emoji, sort_order: emoji.sort_order + 1000 }
|
||||
: emoji
|
||||
);
|
||||
|
||||
const dataByShortName = keyBy(data, 'short_name');
|
||||
const dataByEmoji: { [key: string]: EmojiData } = {};
|
||||
|
||||
export const dataByCategory = mapValues(
|
||||
groupBy(data, ({ category }) => {
|
||||
if (category === 'Activities') {
|
||||
return 'activity';
|
||||
}
|
||||
|
||||
if (category === 'Animals & Nature') {
|
||||
return 'animal';
|
||||
}
|
||||
|
||||
if (category === 'Flags') {
|
||||
return 'flag';
|
||||
}
|
||||
|
||||
if (category === 'Food & Drink') {
|
||||
return 'food';
|
||||
}
|
||||
|
||||
if (category === 'Objects') {
|
||||
return 'object';
|
||||
}
|
||||
|
||||
if (category === 'Travel & Places') {
|
||||
return 'travel';
|
||||
}
|
||||
|
||||
if (category === 'Smileys & Emotion') {
|
||||
return 'emoji';
|
||||
}
|
||||
|
||||
if (category === 'People & Body') {
|
||||
return 'emoji';
|
||||
}
|
||||
|
||||
if (category === 'Symbols') {
|
||||
return 'symbol';
|
||||
}
|
||||
|
||||
return 'misc';
|
||||
}),
|
||||
arr => sortBy(arr, 'sort_order')
|
||||
);
|
||||
|
||||
export function getEmojiData(
|
||||
shortName: keyof typeof dataByShortName,
|
||||
emojiSkinToneDefault: EmojiSkinTone
|
||||
): EmojiData | EmojiSkinVariation {
|
||||
const base = dataByShortName[shortName];
|
||||
const variation = EMOJI_SKIN_TONE_TO_KEY.get(emojiSkinToneDefault);
|
||||
|
||||
if (variation != null && base.skin_variations) {
|
||||
if (base.skin_variations[variation]) {
|
||||
return base.skin_variations[variation];
|
||||
}
|
||||
|
||||
// For emojis that have two people in them which can have diff skin tones
|
||||
// the Map is of SkinTone-SkinTone. If we don't find the correct skin tone
|
||||
// in the list of variations then we assume it is one of those double skin
|
||||
// emojis and we default to both people having same skin.
|
||||
return base.skin_variations[`${variation}-${variation}`];
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
export function unifiedToEmoji(unified: string): string {
|
||||
return unified
|
||||
.split('-')
|
||||
.map(c => String.fromCodePoint(parseInt(c, 16)))
|
||||
.join('');
|
||||
}
|
||||
|
||||
export function convertShortNameToData(
|
||||
shortName: string,
|
||||
skinTone: EmojiSkinTone
|
||||
): EmojiData | undefined {
|
||||
const base = dataByShortName[shortName];
|
||||
|
||||
if (!base) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (skinTone !== EmojiSkinTone.None && base.skin_variations != null) {
|
||||
const toneKey = EMOJI_SKIN_TONE_TO_KEY.get(skinTone);
|
||||
strictAssert(toneKey, `Missing key for skin tone: ${skinTone}`);
|
||||
const variation =
|
||||
base.skin_variations[toneKey] ??
|
||||
base.skin_variations[`${toneKey}-${toneKey}`];
|
||||
if (variation) {
|
||||
return {
|
||||
...base,
|
||||
...variation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
export function convertShortName(
|
||||
shortName: string,
|
||||
skinTone: EmojiSkinTone
|
||||
): string {
|
||||
const emojiData = convertShortNameToData(shortName, skinTone);
|
||||
|
||||
if (!emojiData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return unifiedToEmoji(emojiData.unified);
|
||||
}
|
||||
|
||||
export function emojiToData(emoji: string): EmojiData | undefined {
|
||||
return getOwn(dataByEmoji, emoji);
|
||||
}
|
||||
|
||||
data.forEach(emoji => {
|
||||
const { short_name, short_names, skin_variations } = emoji;
|
||||
|
||||
if (short_names) {
|
||||
short_names.forEach(name => {
|
||||
dataByShortName[name] = emoji;
|
||||
});
|
||||
}
|
||||
|
||||
dataByEmoji[convertShortName(short_name, EmojiSkinTone.None)] = emoji;
|
||||
|
||||
if (skin_variations) {
|
||||
Object.entries(skin_variations).forEach(([tone]) => {
|
||||
const emojiSkinTone = KEY_TO_EMOJI_SKIN_TONE.get(tone);
|
||||
if (emojiSkinTone != null) {
|
||||
dataByEmoji[convertShortName(short_name, emojiSkinTone)] = emoji;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import type { FunStaticEmojiProps } from './FunEmoji.js';
|
||||
import { FunInlineEmoji, FunStaticEmoji } from './FunEmoji.js';
|
||||
import {
|
||||
_getAllEmojiVariantKeys,
|
||||
emojiVariantConstant,
|
||||
EMOJI_VARIANT_KEY_CONSTANTS,
|
||||
getEmojiParentByKey,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
@@ -121,6 +121,10 @@ export function All(props: AllProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const FRIED_SHRIMP = getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.FRIED_SHRIMP
|
||||
);
|
||||
|
||||
export function Inline(): JSX.Element {
|
||||
return (
|
||||
<div style={{ userSelect: 'none' }}>
|
||||
@@ -128,7 +132,7 @@ export function Inline(): JSX.Element {
|
||||
<FunInlineEmoji
|
||||
role="img"
|
||||
aria-label="Fried Shrimp"
|
||||
emoji={emojiVariantConstant('\u{1F364}')}
|
||||
emoji={FRIED_SHRIMP}
|
||||
/>{' '}
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Repellat
|
||||
voluptates, mollitia tempora alias libero repudiandae nesciunt. Deleniti
|
||||
@@ -137,7 +141,7 @@ export function Inline(): JSX.Element {
|
||||
<FunInlineEmoji
|
||||
role="img"
|
||||
aria-label="Fried Shrimp"
|
||||
emoji={emojiVariantConstant('\u{1F364}')}
|
||||
emoji={FRIED_SHRIMP}
|
||||
/>{' '}
|
||||
Consectetur quibusdam accusantium magni ipsum nemo eligendi quisquam
|
||||
dolor, recusandae vero dolore reiciendis doloribus ducimus officiis
|
||||
@@ -146,7 +150,7 @@ export function Inline(): JSX.Element {
|
||||
<FunInlineEmoji
|
||||
role="img"
|
||||
aria-label="Fried Shrimp"
|
||||
emoji={emojiVariantConstant('\u{1F364}')}
|
||||
emoji={FRIED_SHRIMP}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -621,7 +621,7 @@ export function _getAllEmojiVariantKeys(): Iterable<EmojiVariantKey> {
|
||||
return EMOJI_INDEX.variantByKey.keys();
|
||||
}
|
||||
|
||||
export function emojiParentKeyConstant(input: string): EmojiParentKey {
|
||||
function emojiParentKeyConstant(input: string): EmojiParentKey {
|
||||
strictAssert(
|
||||
isEmojiParentValue(input),
|
||||
`Missing emoji parent for value "${input}"`
|
||||
@@ -629,15 +629,44 @@ export function emojiParentKeyConstant(input: string): EmojiParentKey {
|
||||
return getEmojiParentKeyByValue(input);
|
||||
}
|
||||
|
||||
export function emojiVariantConstant(input: string): EmojiVariantData {
|
||||
function emojiVariantKeyConstant(input: string): EmojiVariantKey {
|
||||
strictAssert(
|
||||
isEmojiVariantValue(input),
|
||||
`Missing emoji variant for value "${input}"`
|
||||
);
|
||||
const key = getEmojiVariantKeyByValue(input);
|
||||
return getEmojiVariantByKey(key);
|
||||
return getEmojiVariantKeyByValue(input);
|
||||
}
|
||||
|
||||
export const EMOJI_PARENT_KEY_CONSTANTS = {
|
||||
RED_HEART: emojiParentKeyConstant('\u{2764}\u{FE0F}'),
|
||||
CRYING_FACE: emojiParentKeyConstant('\u{1F622}'),
|
||||
FACE_WITH_TEARS_OF_JOY: emojiParentKeyConstant('\u{1F602}'),
|
||||
FACE_WITH_OPEN_MOUTH: emojiParentKeyConstant('\u{1F62E}'),
|
||||
ENRAGED_FACE: emojiParentKeyConstant('\u{1F621}'),
|
||||
SLIGHTLY_SMILING_FACE: emojiParentKeyConstant('\u{1F642}'),
|
||||
SLIGHTLY_FROWNING_FACE: emojiParentKeyConstant('\u{1F641}'),
|
||||
GRINNING_FACE: emojiParentKeyConstant('\u{1F600}'),
|
||||
FACE_BLOWING_A_KISS: emojiParentKeyConstant('\u{1F618}'),
|
||||
FACE_WITH_STUCK_OUT_TONGUE: emojiParentKeyConstant('\u{1F61B}'),
|
||||
CONFUSED_FACE: emojiParentKeyConstant('\u{1F615}'),
|
||||
NEUTRAL_FACE: emojiParentKeyConstant('\u{1F610}'),
|
||||
WINKING_FACE: emojiParentKeyConstant('\u{1F609}'),
|
||||
ZIPPER_MOUTH_FACE: emojiParentKeyConstant('\u{1F910}'),
|
||||
THUMBS_UP: emojiParentKeyConstant('\u{1F44D}'),
|
||||
THUMBS_DOWN: emojiParentKeyConstant('\u{1F44E}'),
|
||||
RAISED_HAND: emojiParentKeyConstant('\u{270B}'),
|
||||
WAVING_HAND: emojiParentKeyConstant('\u{1F44B}'),
|
||||
HOT_BEVERAGE: emojiParentKeyConstant('\u{2615}'),
|
||||
MOBILE_PHONE_OFF: emojiParentKeyConstant('\u{1F4F4}'),
|
||||
} as const;
|
||||
|
||||
export const EMOJI_VARIANT_KEY_CONSTANTS = {
|
||||
SLIGHTLY_FROWNING_FACE: emojiVariantKeyConstant('\u{1F641}'),
|
||||
GRINNING_FACE_WITH_SMILING_EYES: emojiVariantKeyConstant('\u{1F604}'),
|
||||
GRINNING_CAT_WITH_SMILING_EYES: emojiVariantKeyConstant('\u{1F638}'),
|
||||
FRIED_SHRIMP: emojiVariantKeyConstant('\u{1F364}'),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Completions
|
||||
*/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import * as RemoteConfig from '../../RemoteConfig.js';
|
||||
|
||||
export function isFunPickerEnabled(): boolean {
|
||||
return RemoteConfig.isEnabled('desktop.funPicker');
|
||||
}
|
||||
@@ -48,12 +48,10 @@ import {
|
||||
FunSubNavListBox,
|
||||
FunSubNavListBoxItem,
|
||||
} from '../base/FunSubNav.js';
|
||||
import type { EmojiParentKey, EmojiVariantKey } from '../data/emojis.js';
|
||||
import type { EmojiVariantKey } from '../data/emojis.js';
|
||||
import {
|
||||
EmojiSkinTone,
|
||||
emojiParentKeyConstant,
|
||||
EmojiPickerCategory,
|
||||
emojiVariantConstant,
|
||||
getEmojiParentByKey,
|
||||
getEmojiPickerCategoryParentKeys,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
@@ -61,7 +59,8 @@ import {
|
||||
isEmojiVariantKey,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiSkinToneByVariantKey,
|
||||
EMOJI_PARENT_KEY_CONSTANTS,
|
||||
EMOJI_VARIANT_KEY_CONSTANTS,
|
||||
} from '../data/emojis.js';
|
||||
import { useFunEmojiSearch } from '../useFunEmojiSearch.js';
|
||||
import { FunKeyboard } from '../keyboard/FunKeyboard.js';
|
||||
@@ -163,9 +162,6 @@ function getSelectedSection(
|
||||
|
||||
export type FunEmojiSelection = Readonly<{
|
||||
variantKey: EmojiVariantKey;
|
||||
parentKey: EmojiParentKey;
|
||||
englishShortName: string;
|
||||
skinTone: EmojiSkinTone;
|
||||
}>;
|
||||
|
||||
export type FunPanelEmojisProps = Readonly<{
|
||||
@@ -479,7 +475,9 @@ export function FunPanelEmojis({
|
||||
<FunStaticEmoji
|
||||
size={16}
|
||||
role="presentation"
|
||||
emoji={emojiVariantConstant('\u{1F641}')}
|
||||
emoji={getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.SLIGHTLY_FROWNING_FACE
|
||||
)}
|
||||
/>
|
||||
</FunResultsHeader>
|
||||
</FunResults>
|
||||
@@ -656,10 +654,6 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
return getEmojiVariantByKey(props.value);
|
||||
}, [props.value]);
|
||||
|
||||
const skinTone = useMemo(() => {
|
||||
return getEmojiSkinToneByVariantKey(emojiVariant.key);
|
||||
}, [emojiVariant.key]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (emojiHasSkinToneVariants && emojiSkinToneDefault == null) {
|
||||
@@ -668,9 +662,6 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
}
|
||||
const emojiSelection: FunEmojiSelection = {
|
||||
variantKey: emojiVariant.key,
|
||||
parentKey: emojiParent.key,
|
||||
englishShortName: emojiParent.englishShortNameDefault,
|
||||
skinTone,
|
||||
};
|
||||
const shouldClose =
|
||||
event.nativeEvent.pointerType !== 'mouse' &&
|
||||
@@ -681,9 +672,6 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
emojiHasSkinToneVariants,
|
||||
emojiSkinToneDefault,
|
||||
emojiVariant.key,
|
||||
emojiParent.key,
|
||||
emojiParent.englishShortNameDefault,
|
||||
skinTone,
|
||||
onSelectEmoji,
|
||||
]
|
||||
);
|
||||
@@ -714,19 +702,11 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
onEmojiSkinToneDefaultChange(skinToneSelection);
|
||||
const emojiSelection: FunEmojiSelection = {
|
||||
variantKey: variant.key,
|
||||
parentKey: emojiParent.key,
|
||||
englishShortName: emojiParent.englishShortNameDefault,
|
||||
skinTone: skinToneSelection,
|
||||
};
|
||||
const shouldClose = true;
|
||||
onSelectEmoji(emojiSelection, shouldClose);
|
||||
},
|
||||
[
|
||||
onEmojiSkinToneDefaultChange,
|
||||
emojiParent.key,
|
||||
emojiParent.englishShortNameDefault,
|
||||
onSelectEmoji,
|
||||
]
|
||||
[onEmojiSkinToneDefaultChange, emojiParent.key, onSelectEmoji]
|
||||
);
|
||||
|
||||
const emojiName = useMemo(() => {
|
||||
@@ -836,7 +816,7 @@ function SectionSkinToneHeaderPopover(
|
||||
</FunGridHeaderPopoverHeader>
|
||||
<FunSkinTonesList
|
||||
i18n={i18n}
|
||||
emoji={emojiParentKeyConstant('\u{270B}')}
|
||||
emoji={EMOJI_PARENT_KEY_CONSTANTS.RAISED_HAND}
|
||||
skinTone={null}
|
||||
onSelectSkinTone={handleSelectSkinTone}
|
||||
/>
|
||||
|
||||
@@ -52,7 +52,6 @@ import {
|
||||
FunResultsSpinner,
|
||||
} from '../base/FunResults.js';
|
||||
import { FunStaticEmoji } from '../FunEmoji.js';
|
||||
import { emojiVariantConstant } from '../data/emojis.js';
|
||||
import {
|
||||
FunLightboxPortal,
|
||||
FunLightboxBackdrop,
|
||||
@@ -66,6 +65,10 @@ import type { LocalizerType } from '../../../types/I18N.js';
|
||||
import { isAbortError } from '../../../util/isAbortError.js';
|
||||
import { createLogger } from '../../../logging/log.js';
|
||||
import * as Errors from '../../../types/errors.js';
|
||||
import {
|
||||
EMOJI_VARIANT_KEY_CONSTANTS,
|
||||
getEmojiVariantByKey,
|
||||
} from '../data/emojis.js';
|
||||
|
||||
const log = createLogger('FunPanelGifs');
|
||||
|
||||
@@ -508,7 +511,9 @@ export function FunPanelGifs({
|
||||
<FunStaticEmoji
|
||||
size={16}
|
||||
role="presentation"
|
||||
emoji={emojiVariantConstant('\u{1F641}')}
|
||||
emoji={getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.SLIGHTLY_FROWNING_FACE
|
||||
)}
|
||||
/>
|
||||
</FunResultsHeader>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { VisuallyHidden } from 'react-aria';
|
||||
import type {
|
||||
StickerPackType,
|
||||
StickerType,
|
||||
@@ -55,9 +56,10 @@ import {
|
||||
FunSubNavScroller,
|
||||
} from '../base/FunSubNav.js';
|
||||
import {
|
||||
EMOJI_VARIANT_KEY_CONSTANTS,
|
||||
type EmojiParentKey,
|
||||
emojiVariantConstant,
|
||||
getEmojiParentKeyByValue,
|
||||
getEmojiVariantByKey,
|
||||
isEmojiParentValue,
|
||||
} from '../data/emojis.js';
|
||||
import { FunKeyboard } from '../keyboard/FunKeyboard.js';
|
||||
@@ -455,6 +457,9 @@ export function FunPanelStickers({
|
||||
{onAddStickerPack != null && (
|
||||
<FunSubNavButtons>
|
||||
<FunSubNavButton onClick={onAddStickerPack}>
|
||||
<VisuallyHidden>
|
||||
{i18n('icu:FunPanelStickers__SubNavButton--AddStickerPack')}
|
||||
</VisuallyHidden>
|
||||
<FunSubNavIcon iconClassName="FunSubNav__Icon--Plus" />
|
||||
</FunSubNavButton>
|
||||
</FunSubNavButtons>
|
||||
@@ -475,7 +480,9 @@ export function FunPanelStickers({
|
||||
<FunStaticEmoji
|
||||
size={16}
|
||||
role="presentation"
|
||||
emoji={emojiVariantConstant('\u{1F641}')}
|
||||
emoji={getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.SLIGHTLY_FROWNING_FACE
|
||||
)}
|
||||
/>
|
||||
</FunResultsHeader>
|
||||
</FunResults>
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { Props } from './StickerButton.js';
|
||||
import { StickerButton } from './StickerButton.js';
|
||||
import {
|
||||
createPack,
|
||||
sticker1,
|
||||
sticker2,
|
||||
tallSticker,
|
||||
wideSticker,
|
||||
} from './mocks.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'Components/Stickers/StickerButton',
|
||||
decorators: [
|
||||
storyFn => (
|
||||
<div
|
||||
style={{
|
||||
height: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{storyFn()}
|
||||
</div>
|
||||
),
|
||||
],
|
||||
argTypes: {
|
||||
showIntroduction: { control: { type: 'boolean' } },
|
||||
showPickerHint: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
blessedPacks: [],
|
||||
clearInstalledStickerPack: action('clearInstalledStickerPack'),
|
||||
clearShowIntroduction: action('clearShowIntroduction'),
|
||||
clearShowPickerHint: action('clearShowPickerHint'),
|
||||
i18n,
|
||||
installedPacks: [],
|
||||
knownPacks: [],
|
||||
onClickAddPack: action('onClickAddPack'),
|
||||
onPickSticker: action('onPickSticker'),
|
||||
receivedPacks: [],
|
||||
recentStickers: [],
|
||||
showIntroduction: false,
|
||||
showPickerHint: false,
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
const receivedPacks = [
|
||||
createPack({ id: 'received-pack-1', status: 'downloaded' }, sticker1),
|
||||
createPack({ id: 'received-pack-2', status: 'downloaded' }, sticker2),
|
||||
];
|
||||
|
||||
const installedPacks = [
|
||||
createPack({ id: 'installed-pack-1', status: 'installed' }, sticker1),
|
||||
createPack({ id: 'installed-pack-2', status: 'installed' }, sticker2),
|
||||
];
|
||||
|
||||
const blessedPacks = [
|
||||
createPack(
|
||||
{ id: 'blessed-pack-1', status: 'downloaded', isBlessed: true },
|
||||
sticker1
|
||||
),
|
||||
createPack(
|
||||
{ id: 'blessed-pack-2', status: 'downloaded', isBlessed: true },
|
||||
sticker2
|
||||
),
|
||||
];
|
||||
|
||||
const knownPacks = [
|
||||
createPack({ id: 'known-pack-1', status: 'known' }, sticker1),
|
||||
createPack({ id: 'known-pack-2', status: 'known' }, sticker2),
|
||||
];
|
||||
|
||||
export function OnlyInstalled(args: Props): JSX.Element {
|
||||
return <StickerButton {...args} installedPacks={installedPacks} />;
|
||||
}
|
||||
|
||||
export function OnlyReceived(args: Props): JSX.Element {
|
||||
return <StickerButton {...args} receivedPacks={receivedPacks} />;
|
||||
}
|
||||
|
||||
export function OnlyKnown(args: Props): JSX.Element {
|
||||
return <StickerButton {...args} knownPacks={knownPacks} />;
|
||||
}
|
||||
|
||||
export function OnlyBlessed(args: Props): JSX.Element {
|
||||
return <StickerButton {...args} blessedPacks={blessedPacks} />;
|
||||
}
|
||||
|
||||
export function NoPacks(args: Props): JSX.Element {
|
||||
return <StickerButton {...args} />;
|
||||
}
|
||||
|
||||
export function InstalledPackTooltip(args: Props): JSX.Element {
|
||||
return (
|
||||
<StickerButton
|
||||
{...args}
|
||||
installedPacks={installedPacks}
|
||||
installedPack={installedPacks[0]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function InstalledPackTooltipWide(args: Props): JSX.Element {
|
||||
const installedPack = createPack({ id: 'installed-pack-wide' }, wideSticker);
|
||||
|
||||
return (
|
||||
<StickerButton
|
||||
{...args}
|
||||
installedPacks={[installedPack]}
|
||||
installedPack={installedPack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function InstalledPackTooltipTall(args: Props): JSX.Element {
|
||||
const installedPack = createPack({ id: 'installed-pack-tall' }, tallSticker);
|
||||
return (
|
||||
<StickerButton
|
||||
{...args}
|
||||
installedPacks={[installedPack]}
|
||||
installedPack={installedPack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NewInstallTooltip(args: Props): JSX.Element {
|
||||
return (
|
||||
<StickerButton {...args} installedPacks={installedPacks} showIntroduction />
|
||||
);
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import lodash from 'lodash';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type {
|
||||
StickerPackType,
|
||||
StickerType,
|
||||
} from '../../state/ducks/stickers.js';
|
||||
import type { LocalizerType } from '../../types/Util.js';
|
||||
import type { Theme } from '../../util/theme.js';
|
||||
import { StickerPicker } from './StickerPicker.js';
|
||||
import { countStickers } from './lib.js';
|
||||
import { offsetDistanceModifier } from '../../util/popperUtil.js';
|
||||
import { themeClassName } from '../../util/theme.js';
|
||||
import { handleOutsideClick } from '../../util/handleOutsideClick.js';
|
||||
import * as KeyboardLayout from '../../services/keyboardLayout.js';
|
||||
import { useRefMerger } from '../../hooks/useRefMerger.js';
|
||||
import { UserText } from '../UserText.js';
|
||||
|
||||
const { get, noop } = lodash;
|
||||
|
||||
export type OwnProps = {
|
||||
readonly className?: string;
|
||||
readonly i18n: LocalizerType;
|
||||
readonly receivedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly installedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly blessedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly knownPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly installedPack?: StickerPackType | null;
|
||||
readonly recentStickers: ReadonlyArray<StickerType>;
|
||||
readonly onOpenStateChanged?: (isOpen: boolean) => void;
|
||||
readonly clearInstalledStickerPack: () => unknown;
|
||||
readonly onClickAddPack?: () => unknown;
|
||||
readonly onPickSticker: (
|
||||
packId: string,
|
||||
stickerId: number,
|
||||
url: string
|
||||
) => unknown;
|
||||
readonly onPickTimeSticker?: (style: 'analog' | 'digital') => unknown;
|
||||
readonly showIntroduction?: boolean;
|
||||
readonly clearShowIntroduction: () => unknown;
|
||||
readonly showPickerHint: boolean;
|
||||
readonly clearShowPickerHint: () => unknown;
|
||||
readonly position?: 'top-end' | 'top-start';
|
||||
readonly theme?: Theme;
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
||||
export const StickerButton = React.memo(function StickerButtonInner({
|
||||
className,
|
||||
i18n,
|
||||
clearInstalledStickerPack,
|
||||
onClickAddPack,
|
||||
onPickSticker,
|
||||
onPickTimeSticker,
|
||||
recentStickers,
|
||||
onOpenStateChanged,
|
||||
receivedPacks,
|
||||
installedPack,
|
||||
installedPacks,
|
||||
blessedPacks,
|
||||
knownPacks,
|
||||
showIntroduction,
|
||||
clearShowIntroduction,
|
||||
showPickerHint,
|
||||
clearShowPickerHint,
|
||||
position = 'top-end',
|
||||
theme,
|
||||
}: Props) {
|
||||
const [open, internalSetOpen] = React.useState(false);
|
||||
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean) => {
|
||||
internalSetOpen(value);
|
||||
if (onOpenStateChanged) {
|
||||
onOpenStateChanged(value);
|
||||
}
|
||||
},
|
||||
[internalSetOpen, onOpenStateChanged]
|
||||
);
|
||||
|
||||
const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>(null);
|
||||
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const refMerger = useRefMerger();
|
||||
|
||||
const handleClickButton = React.useCallback(() => {
|
||||
// Clear tooltip state
|
||||
clearInstalledStickerPack();
|
||||
clearShowIntroduction();
|
||||
|
||||
// Handle button click
|
||||
if (installedPacks.length === 0) {
|
||||
onClickAddPack?.();
|
||||
} else if (popperRoot) {
|
||||
setOpen(false);
|
||||
} else {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [
|
||||
clearInstalledStickerPack,
|
||||
clearShowIntroduction,
|
||||
installedPacks,
|
||||
onClickAddPack,
|
||||
popperRoot,
|
||||
setOpen,
|
||||
]);
|
||||
|
||||
const handlePickSticker = React.useCallback(
|
||||
(packId: string, stickerId: number, url: string) => {
|
||||
setOpen(false);
|
||||
onPickSticker(packId, stickerId, url);
|
||||
},
|
||||
[setOpen, onPickSticker]
|
||||
);
|
||||
|
||||
const handlePickTimeSticker = React.useCallback(
|
||||
(style: 'analog' | 'digital') => {
|
||||
setOpen(false);
|
||||
onPickTimeSticker?.(style);
|
||||
},
|
||||
[setOpen, onPickTimeSticker]
|
||||
);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const handleClickAddPack = React.useCallback(() => {
|
||||
setOpen(false);
|
||||
if (showPickerHint) {
|
||||
clearShowPickerHint();
|
||||
}
|
||||
onClickAddPack?.();
|
||||
}, [onClickAddPack, showPickerHint, setOpen, clearShowPickerHint]);
|
||||
|
||||
const handleClearIntroduction = React.useCallback(() => {
|
||||
clearInstalledStickerPack();
|
||||
clearShowIntroduction();
|
||||
}, [clearInstalledStickerPack, clearShowIntroduction]);
|
||||
|
||||
// Create popper root and handle outside clicks
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
const root = document.createElement('div');
|
||||
setPopperRoot(root);
|
||||
document.body.appendChild(root);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(root);
|
||||
setPopperRoot(null);
|
||||
};
|
||||
}
|
||||
|
||||
return noop;
|
||||
}, [open, setOpen, setPopperRoot]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
return handleOutsideClick(
|
||||
target => {
|
||||
const targetElement = target as HTMLElement;
|
||||
const targetClassName = targetElement
|
||||
? targetElement.className || ''
|
||||
: '';
|
||||
|
||||
// We need to special-case sticker picker header buttons, because they can
|
||||
// disappear after being clicked, which breaks the .contains() check below.
|
||||
const isMissingButtonClass =
|
||||
!targetClassName ||
|
||||
targetClassName.indexOf('module-sticker-picker__header__button') < 0;
|
||||
|
||||
if (!isMissingButtonClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
return true;
|
||||
},
|
||||
{
|
||||
containerElements: [popperRoot, buttonRef],
|
||||
name: 'StickerButton',
|
||||
}
|
||||
);
|
||||
}, [open, popperRoot, setOpen]);
|
||||
|
||||
// Install keyboard shortcut to open sticker picker
|
||||
React.useEffect(() => {
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
const { ctrlKey, metaKey, shiftKey } = event;
|
||||
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
|
||||
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
|
||||
const commandOrCtrl = commandKey || controlKey;
|
||||
const key = KeyboardLayout.lookup(event);
|
||||
|
||||
// We don't want to open up if the conversation has any panels open
|
||||
const panels = document.querySelectorAll('.conversation .panel');
|
||||
if (panels && panels.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandOrCtrl && shiftKey && (key === 'g' || key === 'G')) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
setOpen(!open);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
}, [open, setOpen]);
|
||||
|
||||
// Clear the installed pack after one minute
|
||||
React.useEffect(() => {
|
||||
if (installedPack) {
|
||||
const timerId = setTimeout(clearInstalledStickerPack, 10 * 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timerId);
|
||||
};
|
||||
}
|
||||
|
||||
return noop;
|
||||
}, [installedPack, clearInstalledStickerPack]);
|
||||
|
||||
if (
|
||||
countStickers({
|
||||
knownPacks,
|
||||
blessedPacks,
|
||||
installedPacks,
|
||||
receivedPacks,
|
||||
}) === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<button
|
||||
type="button"
|
||||
ref={refMerger(buttonRef, ref)}
|
||||
onClick={handleClickButton}
|
||||
className={classNames(
|
||||
{
|
||||
'module-sticker-button__button': true,
|
||||
'module-sticker-button__button--active': open,
|
||||
},
|
||||
className
|
||||
)}
|
||||
aria-label={i18n('icu:stickers--StickerPicker--Open')}
|
||||
/>
|
||||
)}
|
||||
</Reference>
|
||||
{!open && !showIntroduction && installedPack ? (
|
||||
<Popper
|
||||
placement={position}
|
||||
key={installedPack.id}
|
||||
modifiers={[offsetDistanceModifier(6)]}
|
||||
>
|
||||
{({ ref, style, placement, arrowProps }) => (
|
||||
<div className={theme ? themeClassName(theme) : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
ref={ref}
|
||||
style={style}
|
||||
className="module-sticker-button__tooltip"
|
||||
onClick={clearInstalledStickerPack}
|
||||
>
|
||||
{installedPack.cover ? (
|
||||
<img
|
||||
className="module-sticker-button__tooltip__image"
|
||||
src={installedPack.cover.url}
|
||||
alt={installedPack.title}
|
||||
/>
|
||||
) : (
|
||||
<div className="module-sticker-button__tooltip__image-placeholder" />
|
||||
)}
|
||||
<span className="module-sticker-button__tooltip__text">
|
||||
<span className="module-sticker-button__tooltip__text__title">
|
||||
<UserText text={installedPack.title} />
|
||||
</span>{' '}
|
||||
installed
|
||||
</span>
|
||||
<div
|
||||
ref={arrowProps.ref}
|
||||
style={arrowProps.style}
|
||||
className={classNames(
|
||||
'module-sticker-button__tooltip__triangle',
|
||||
`module-sticker-button__tooltip__triangle--${placement}`
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Popper>
|
||||
) : null}
|
||||
{!open && showIntroduction ? (
|
||||
<Popper placement={position} modifiers={[offsetDistanceModifier(6)]}>
|
||||
{({ ref, style, placement, arrowProps }) => (
|
||||
<div className={theme ? themeClassName(theme) : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
ref={ref}
|
||||
style={style}
|
||||
className={classNames(
|
||||
'module-sticker-button__tooltip',
|
||||
'module-sticker-button__tooltip--introduction'
|
||||
)}
|
||||
onClick={handleClearIntroduction}
|
||||
>
|
||||
<img
|
||||
className="module-sticker-button__tooltip--introduction__image"
|
||||
srcSet="images/sticker_splash@1x.png 1x, images/sticker_splash@2x.png 2x"
|
||||
alt={i18n(
|
||||
'icu:stickers--StickerManager--Introduction--Image'
|
||||
)}
|
||||
/>
|
||||
<div className="module-sticker-button__tooltip--introduction__meta">
|
||||
<div className="module-sticker-button__tooltip--introduction__meta__title">
|
||||
{i18n('icu:stickers--StickerManager--Introduction--Title')}
|
||||
</div>
|
||||
<div className="module-sticker-button__tooltip--introduction__meta__subtitle">
|
||||
{i18n('icu:stickers--StickerManager--Introduction--Body')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-sticker-button__tooltip--introduction__close">
|
||||
<button
|
||||
type="button"
|
||||
className="module-sticker-button__tooltip--introduction__close__button"
|
||||
onClick={handleClearIntroduction}
|
||||
aria-label={i18n('icu:close')}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={arrowProps.ref}
|
||||
style={arrowProps.style}
|
||||
className={classNames(
|
||||
'module-sticker-button__tooltip__triangle',
|
||||
'module-sticker-button__tooltip__triangle--introduction',
|
||||
`module-sticker-button__tooltip__triangle--${placement}`
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Popper>
|
||||
) : null}
|
||||
{open && popperRoot
|
||||
? createPortal(
|
||||
<Popper placement={position}>
|
||||
{({ ref, style }) => (
|
||||
<div className={theme ? themeClassName(theme) : undefined}>
|
||||
<StickerPicker
|
||||
ref={ref}
|
||||
i18n={i18n}
|
||||
style={style}
|
||||
packs={installedPacks}
|
||||
onClose={handleClose}
|
||||
onClickAddPack={
|
||||
onClickAddPack ? handleClickAddPack : undefined
|
||||
}
|
||||
onPickSticker={handlePickSticker}
|
||||
onPickTimeSticker={
|
||||
onPickTimeSticker ? handlePickTimeSticker : undefined
|
||||
}
|
||||
recentStickers={recentStickers}
|
||||
showPickerHint={showPickerHint}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Popper>,
|
||||
popperRoot
|
||||
)
|
||||
: null}
|
||||
</Manager>
|
||||
);
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { Props } from './StickerPicker.js';
|
||||
import { StickerPicker } from './StickerPicker.js';
|
||||
import { abeSticker, createPack, packs, recentStickers } from './mocks.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'Components/Stickers/StickerPicker',
|
||||
component: StickerPicker,
|
||||
argTypes: {
|
||||
showPickerHint: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
onClickAddPack: action('onClickAddPack'),
|
||||
onClose: action('onClose'),
|
||||
onPickSticker: action('onPickSticker'),
|
||||
packs: [],
|
||||
recentStickers: [],
|
||||
showPickerHint: false,
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
export function Full(args: Props): JSX.Element {
|
||||
return (
|
||||
<StickerPicker {...args} packs={packs} recentStickers={recentStickers} />
|
||||
);
|
||||
}
|
||||
|
||||
export function PickerHint(args: Props): JSX.Element {
|
||||
return (
|
||||
<StickerPicker
|
||||
{...args}
|
||||
packs={packs}
|
||||
recentStickers={recentStickers}
|
||||
showPickerHint
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoRecentStickers(args: Props): JSX.Element {
|
||||
return <StickerPicker {...args} packs={packs} />;
|
||||
}
|
||||
|
||||
export function Empty(args: Props): JSX.Element {
|
||||
return <StickerPicker {...args} />;
|
||||
}
|
||||
|
||||
export function PendingDownload(args: Props): JSX.Element {
|
||||
const pack = createPack(
|
||||
{ status: 'pending', stickers: [abeSticker] },
|
||||
abeSticker
|
||||
);
|
||||
|
||||
return <StickerPicker {...args} packs={[pack]} />;
|
||||
}
|
||||
|
||||
export function Error(args: Props): JSX.Element {
|
||||
const pack = createPack(
|
||||
{ status: 'error', stickers: [abeSticker] },
|
||||
abeSticker
|
||||
);
|
||||
|
||||
return <StickerPicker {...args} packs={[pack]} />;
|
||||
}
|
||||
|
||||
export function NoCover(args: Props): JSX.Element {
|
||||
const pack = createPack({ status: 'error', stickers: [abeSticker] });
|
||||
return <StickerPicker {...args} packs={[pack]} />;
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
import { FocusScope } from 'react-aria';
|
||||
import { useRestoreFocus } from '../../hooks/useRestoreFocus.js';
|
||||
import type {
|
||||
StickerPackType,
|
||||
StickerType,
|
||||
} from '../../state/ducks/stickers.js';
|
||||
import type { LocalizerType } from '../../types/Util.js';
|
||||
import { getDateTimeFormatter } from '../../util/formatTimestamp.js';
|
||||
import { getAnalogTime } from '../../util/getAnalogTime.js';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly onClose: () => unknown;
|
||||
readonly onClickAddPack?: () => unknown;
|
||||
readonly onPickSticker: (
|
||||
packId: string,
|
||||
stickerId: number,
|
||||
url: string
|
||||
) => unknown;
|
||||
readonly onPickTimeSticker?: (style: 'analog' | 'digital') => unknown;
|
||||
readonly packs: ReadonlyArray<StickerPackType>;
|
||||
readonly recentStickers: ReadonlyArray<StickerType>;
|
||||
readonly showPickerHint?: boolean;
|
||||
};
|
||||
|
||||
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||
|
||||
function useTabs<T>(tabs: ReadonlyArray<T>, initialTab = tabs[0]) {
|
||||
const [tab, setTab] = React.useState(initialTab);
|
||||
const handlers = React.useMemo(
|
||||
() =>
|
||||
tabs.map(t => () => {
|
||||
setTab(t);
|
||||
}),
|
||||
[tabs]
|
||||
);
|
||||
|
||||
return [tab, handlers] as [T, ReadonlyArray<() => void>];
|
||||
}
|
||||
|
||||
const PACKS_PAGE_SIZE = 7;
|
||||
const PACK_ICON_WIDTH = 32;
|
||||
const PACK_PAGE_WIDTH = PACKS_PAGE_SIZE * PACK_ICON_WIDTH;
|
||||
|
||||
function getPacksPageOffset(page: number, packs: number): number {
|
||||
if (page === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isLastPacksPage(page, packs)) {
|
||||
return (
|
||||
PACK_PAGE_WIDTH * (Math.floor(packs / PACKS_PAGE_SIZE) - 1) +
|
||||
((packs % PACKS_PAGE_SIZE) - 1) * PACK_ICON_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
return page * PACK_ICON_WIDTH * PACKS_PAGE_SIZE;
|
||||
}
|
||||
|
||||
function isLastPacksPage(page: number, packs: number): boolean {
|
||||
return page === Math.floor(packs / PACKS_PAGE_SIZE);
|
||||
}
|
||||
|
||||
export const StickerPicker = React.memo(
|
||||
React.forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{
|
||||
i18n,
|
||||
packs,
|
||||
recentStickers,
|
||||
onClose,
|
||||
onClickAddPack,
|
||||
onPickSticker,
|
||||
onPickTimeSticker,
|
||||
showPickerHint,
|
||||
style,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const tabIds = React.useMemo(
|
||||
() => ['recents', ...packs.map(({ id }) => id)],
|
||||
[packs]
|
||||
);
|
||||
const [currentTab, [recentsHandler, ...packsHandlers]] = useTabs(
|
||||
tabIds,
|
||||
// If there are no recent stickers,
|
||||
// default to the first sticker pack,
|
||||
// unless there are no sticker packs.
|
||||
tabIds[recentStickers.length > 0 ? 0 : Math.min(1, tabIds.length)]
|
||||
);
|
||||
const selectedPack = packs.find(({ id }) => id === currentTab);
|
||||
const {
|
||||
stickers = recentStickers,
|
||||
title: packTitle = 'Recent Stickers',
|
||||
} = selectedPack || {};
|
||||
|
||||
const [isUsingKeyboard, setIsUsingKeyboard] = React.useState(false);
|
||||
const [packsPage, setPacksPage] = React.useState(0);
|
||||
const onClickPrevPackPage = React.useCallback(() => {
|
||||
setPacksPage(i => i - 1);
|
||||
}, [setPacksPage]);
|
||||
const onClickNextPackPage = React.useCallback(() => {
|
||||
setPacksPage(i => i + 1);
|
||||
}, [setPacksPage]);
|
||||
|
||||
// Handle escape key
|
||||
React.useEffect(() => {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Tab') {
|
||||
// We do NOT prevent default here to allow Tab to be used normally
|
||||
|
||||
setIsUsingKeyboard(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handler);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
// Focus popup on after initial render, restore focus on teardown
|
||||
const [focusRef] = useRestoreFocus();
|
||||
|
||||
const hasPacks = packs.length > 0;
|
||||
const isRecents = hasPacks && currentTab === 'recents';
|
||||
const hasTimeStickers = isRecents && onPickTimeSticker;
|
||||
const isEmpty = stickers.length === 0 && !hasTimeStickers;
|
||||
const addPackRef = isEmpty ? focusRef : undefined;
|
||||
const downloadError =
|
||||
selectedPack &&
|
||||
selectedPack.status === 'error' &&
|
||||
selectedPack.stickerCount !== selectedPack.stickers.length;
|
||||
const pendingCount =
|
||||
selectedPack && selectedPack.status === 'pending'
|
||||
? selectedPack.stickerCount - stickers.length
|
||||
: 0;
|
||||
|
||||
const showPendingText = pendingCount > 0;
|
||||
const showDownloadErrorText = downloadError;
|
||||
const showEmptyText = !downloadError && isEmpty;
|
||||
const showText =
|
||||
showPendingText || showDownloadErrorText || showEmptyText;
|
||||
const showLongText = showPickerHint;
|
||||
const analogTime = getAnalogTime();
|
||||
|
||||
return (
|
||||
<FocusScope contain>
|
||||
<div className="module-sticker-picker" ref={ref} style={style}>
|
||||
<div className="module-sticker-picker__header">
|
||||
<div className="module-sticker-picker__header__packs">
|
||||
<div
|
||||
className="module-sticker-picker__header__packs__slider"
|
||||
style={{
|
||||
transform: `translateX(-${getPacksPageOffset(
|
||||
packsPage,
|
||||
packs.length
|
||||
)}px)`,
|
||||
}}
|
||||
>
|
||||
{hasPacks ? (
|
||||
<button
|
||||
aria-pressed={currentTab === 'recents'}
|
||||
type="button"
|
||||
onClick={recentsHandler}
|
||||
className={classNames({
|
||||
'module-sticker-picker__header__button': true,
|
||||
'module-sticker-picker__header__button--recents': true,
|
||||
'module-sticker-picker__header__button--selected':
|
||||
currentTab === 'recents',
|
||||
})}
|
||||
aria-label={i18n('icu:stickers--StickerPicker--Recents')}
|
||||
/>
|
||||
) : null}
|
||||
{packs.map((pack, i) => (
|
||||
<button
|
||||
aria-pressed={currentTab === pack.id}
|
||||
type="button"
|
||||
key={pack.id}
|
||||
onClick={packsHandlers[i]}
|
||||
className={classNames(
|
||||
'module-sticker-picker__header__button',
|
||||
{
|
||||
'module-sticker-picker__header__button--selected':
|
||||
currentTab === pack.id,
|
||||
'module-sticker-picker__header__button--error':
|
||||
pack.status === 'error',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{pack.cover ? (
|
||||
<img
|
||||
className="module-sticker-picker__header__button__image"
|
||||
src={pack.cover.url}
|
||||
alt={pack.title}
|
||||
title={pack.title}
|
||||
/>
|
||||
) : (
|
||||
<div className="module-sticker-picker__header__button__image-placeholder" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!isUsingKeyboard && packsPage > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-sticker-picker__header__button',
|
||||
'module-sticker-picker__header__button--prev-page'
|
||||
)}
|
||||
onClick={onClickPrevPackPage}
|
||||
aria-label={i18n('icu:stickers--StickerPicker--PrevPage')}
|
||||
/>
|
||||
) : null}
|
||||
{!isUsingKeyboard &&
|
||||
!isLastPacksPage(packsPage, packs.length) ? (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-sticker-picker__header__button',
|
||||
'module-sticker-picker__header__button--next-page'
|
||||
)}
|
||||
onClick={onClickNextPackPage}
|
||||
aria-label={i18n('icu:stickers--StickerPicker--NextPage')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{onClickAddPack && (
|
||||
<button
|
||||
type="button"
|
||||
ref={addPackRef}
|
||||
className={classNames(
|
||||
'module-sticker-picker__header__button',
|
||||
'module-sticker-picker__header__button--add-pack',
|
||||
{
|
||||
'module-sticker-picker__header__button--hint':
|
||||
showPickerHint,
|
||||
}
|
||||
)}
|
||||
onClick={onClickAddPack}
|
||||
aria-label={i18n('icu:stickers--StickerPicker--AddPack')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames('module-sticker-picker__body', {
|
||||
'module-sticker-picker__body--empty': isEmpty,
|
||||
})}
|
||||
>
|
||||
{showPickerHint ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-sticker-picker__body__text',
|
||||
'module-sticker-picker__body__text--hint',
|
||||
{
|
||||
'module-sticker-picker__body__text--pin': showEmptyText,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{i18n('icu:stickers--StickerPicker--Hint')}
|
||||
</div>
|
||||
) : null}
|
||||
{!hasPacks ? (
|
||||
<div className="module-sticker-picker__body__text">
|
||||
{i18n('icu:stickers--StickerPicker--NoPacks')}
|
||||
</div>
|
||||
) : null}
|
||||
{pendingCount > 0 ? (
|
||||
<div className="module-sticker-picker__body__text">
|
||||
{i18n('icu:stickers--StickerPicker--DownloadPending')}
|
||||
</div>
|
||||
) : null}
|
||||
{downloadError ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-sticker-picker__body__text',
|
||||
'module-sticker-picker__body__text--error'
|
||||
)}
|
||||
>
|
||||
{stickers.length > 0
|
||||
? i18n('icu:stickers--StickerPicker--DownloadError')
|
||||
: i18n('icu:stickers--StickerPicker--Empty')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasPacks && showEmptyText ? (
|
||||
<div
|
||||
className={classNames('module-sticker-picker__body__text', {
|
||||
'module-sticker-picker__body__text--error': !isRecents,
|
||||
})}
|
||||
>
|
||||
{isRecents
|
||||
? i18n('icu:stickers--StickerPicker--NoRecents')
|
||||
: i18n('icu:stickers--StickerPicker--Empty')}
|
||||
</div>
|
||||
) : null}
|
||||
{!isEmpty ? (
|
||||
<div className="module-sticker-picker__body__content">
|
||||
{isRecents && onPickTimeSticker && (
|
||||
<div className="module-sticker-picker__recents">
|
||||
<strong className="module-sticker-picker__recents__title">
|
||||
{i18n('icu:stickers__StickerPicker__featured')}
|
||||
</strong>
|
||||
<div className="module-sticker-picker__body__grid">
|
||||
<button
|
||||
type="button"
|
||||
className="module-sticker-picker__body__cell module-sticker-picker__time--digital"
|
||||
onClick={() => onPickTimeSticker('digital')}
|
||||
>
|
||||
{getDateTimeFormatter({
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
})
|
||||
.formatToParts(Date.now())
|
||||
.filter(x => x.type !== 'dayPeriod')
|
||||
.reduce((acc, { value }) => `${acc}${value}`, '')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label={i18n(
|
||||
'icu:stickers__StickerPicker__analog-time'
|
||||
)}
|
||||
className="module-sticker-picker__body__cell module-sticker-picker__time--analog"
|
||||
onClick={() => onPickTimeSticker('analog')}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="module-sticker-picker__time--analog__hour"
|
||||
style={{
|
||||
transform: `rotate(${analogTime.hour}deg)`,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="module-sticker-picker__time--analog__minute"
|
||||
style={{
|
||||
transform: `rotate(${analogTime.minute}deg)`,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{stickers.length > 0 && (
|
||||
<strong className="module-sticker-picker__recents__title">
|
||||
{i18n('icu:stickers__StickerPicker__recent')}
|
||||
</strong>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames('module-sticker-picker__body__grid', {
|
||||
'module-sticker-picker__body__content--under-text':
|
||||
showText,
|
||||
'module-sticker-picker__body__content--under-long-text':
|
||||
showLongText,
|
||||
})}
|
||||
>
|
||||
{stickers.map(({ packId, id, url }, index: number) => {
|
||||
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
ref={maybeFocusRef}
|
||||
key={`${packId}-${id}`}
|
||||
className="module-sticker-picker__body__cell"
|
||||
onClick={() => onPickSticker(packId, id, url)}
|
||||
>
|
||||
<img
|
||||
className="module-sticker-picker__body__cell__image"
|
||||
src={url}
|
||||
alt={packTitle}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{Array(pendingCount)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={i}
|
||||
className="module-sticker-picker__body__cell__placeholder"
|
||||
role="presentation"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</FocusScope>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { StickerPackType } from '../../state/ducks/stickers.js';
|
||||
|
||||
// This function exists to force stickers to be counted consistently wherever
|
||||
// they are counted (TypeScript ensures that all data is named and provided)
|
||||
export function countStickers(o: {
|
||||
knownPacks: ReadonlyArray<StickerPackType>;
|
||||
blessedPacks: ReadonlyArray<StickerPackType>;
|
||||
installedPacks: ReadonlyArray<StickerPackType>;
|
||||
receivedPacks: ReadonlyArray<StickerPackType>;
|
||||
}): number {
|
||||
return (
|
||||
o.knownPacks.length +
|
||||
o.blessedPacks.length +
|
||||
o.installedPacks.length +
|
||||
o.receivedPacks.length
|
||||
);
|
||||
}
|
||||
@@ -6,12 +6,12 @@ import Emitter from '@signalapp/quill-cjs/core/emitter.js';
|
||||
import type Quill from '@signalapp/quill-cjs';
|
||||
|
||||
import { createLogger } from '../../logging/log.js';
|
||||
import type { EmojiData } from '../../components/emoji/lib.js';
|
||||
import type { EmojiParentKey } from '../../components/fun/data/emojis.js';
|
||||
import {
|
||||
convertShortName,
|
||||
convertShortNameToData,
|
||||
} from '../../components/emoji/lib.js';
|
||||
import { EmojiSkinTone } from '../../components/fun/data/emojis.js';
|
||||
EMOJI_PARENT_KEY_CONSTANTS,
|
||||
EmojiSkinTone,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
} from '../../components/fun/data/emojis.js';
|
||||
|
||||
const log = createLogger('index');
|
||||
|
||||
@@ -19,26 +19,28 @@ export type AutoSubstituteAsciiEmojisOptions = {
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
};
|
||||
|
||||
const emojiMap: Record<string, string> = {
|
||||
':-)': 'slightly_smiling_face',
|
||||
':-(': 'slightly_frowning_face',
|
||||
':-D': 'grinning',
|
||||
':-*': 'kissing_heart',
|
||||
':-P': 'stuck_out_tongue',
|
||||
':-p': 'stuck_out_tongue',
|
||||
":'(": 'cry',
|
||||
':-\\': 'confused',
|
||||
':-|': 'neutral_face',
|
||||
';-)': 'wink',
|
||||
'(Y)': '+1',
|
||||
'(N)': '-1',
|
||||
'(y)': '+1',
|
||||
'(n)': '-1',
|
||||
'<3': 'heart',
|
||||
'^_^': 'grin',
|
||||
type EmojiShortcutMap = Partial<Record<string, EmojiParentKey>>;
|
||||
|
||||
const emojiShortcutMap: EmojiShortcutMap = {
|
||||
':-)': EMOJI_PARENT_KEY_CONSTANTS.SLIGHTLY_SMILING_FACE,
|
||||
':-(': EMOJI_PARENT_KEY_CONSTANTS.SLIGHTLY_FROWNING_FACE,
|
||||
':-D': EMOJI_PARENT_KEY_CONSTANTS.GRINNING_FACE,
|
||||
':-*': EMOJI_PARENT_KEY_CONSTANTS.FACE_BLOWING_A_KISS,
|
||||
':-P': EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_STUCK_OUT_TONGUE,
|
||||
':-p': EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_STUCK_OUT_TONGUE,
|
||||
":'(": EMOJI_PARENT_KEY_CONSTANTS.CRYING_FACE,
|
||||
':-\\': EMOJI_PARENT_KEY_CONSTANTS.CONFUSED_FACE,
|
||||
':-|': EMOJI_PARENT_KEY_CONSTANTS.NEUTRAL_FACE,
|
||||
';-)': EMOJI_PARENT_KEY_CONSTANTS.WINKING_FACE,
|
||||
'(Y)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
'(N)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
'(y)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
'(n)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_DOWN,
|
||||
'<3': EMOJI_PARENT_KEY_CONSTANTS.RED_HEART,
|
||||
'^_^': EMOJI_PARENT_KEY_CONSTANTS.GRINNING_FACE,
|
||||
};
|
||||
|
||||
function buildRegexp(obj: Record<string, string>): RegExp {
|
||||
function buildRegexp(obj: EmojiShortcutMap): RegExp {
|
||||
const sanitizedKeys = Object.keys(obj).map(x =>
|
||||
x.replace(/([^a-zA-Z0-9])/g, '\\$1')
|
||||
);
|
||||
@@ -46,7 +48,7 @@ function buildRegexp(obj: Record<string, string>): RegExp {
|
||||
return new RegExp(`(${sanitizedKeys.join('|')})$`);
|
||||
}
|
||||
|
||||
const EMOJI_REGEXP = buildRegexp(emojiMap);
|
||||
const EMOJI_REGEXP = buildRegexp(emojiShortcutMap);
|
||||
|
||||
export class AutoSubstituteAsciiEmojis {
|
||||
options: AutoSubstituteAsciiEmojisOptions;
|
||||
@@ -101,15 +103,11 @@ export class AutoSubstituteAsciiEmojis {
|
||||
}
|
||||
|
||||
const [, textEmoji] = match;
|
||||
const emojiName = emojiMap[textEmoji];
|
||||
const emojiParentKey = emojiShortcutMap[textEmoji];
|
||||
|
||||
const emojiData = convertShortNameToData(
|
||||
emojiName,
|
||||
this.options.emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
);
|
||||
if (emojiData) {
|
||||
if (emojiParentKey != null) {
|
||||
this.insertEmoji(
|
||||
emojiData,
|
||||
emojiParentKey,
|
||||
range.index - textEmoji.length,
|
||||
textEmoji.length,
|
||||
textEmoji
|
||||
@@ -118,20 +116,20 @@ export class AutoSubstituteAsciiEmojis {
|
||||
}
|
||||
|
||||
insertEmoji(
|
||||
emojiData: EmojiData,
|
||||
emojiParentKey: EmojiParentKey,
|
||||
index: number,
|
||||
range: number,
|
||||
source: string
|
||||
): void {
|
||||
const emoji = convertShortName(
|
||||
emojiData.short_name,
|
||||
const emojiVariant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
emojiParentKey,
|
||||
this.options.emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
);
|
||||
const delta = new Delta()
|
||||
.retain(index)
|
||||
.delete(range)
|
||||
.insert({
|
||||
emoji: { value: emoji, source },
|
||||
emoji: { value: emojiVariant.value, source },
|
||||
});
|
||||
this.quill.updateContents(delta, 'api');
|
||||
this.quill.setSelection(index + 1, 0);
|
||||
|
||||
@@ -10,15 +10,13 @@ import { Popper } from 'react-popper';
|
||||
import classNames from 'classnames';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { VirtualElement } from '@popperjs/core';
|
||||
import { convertShortName } from '../../components/emoji/lib.js';
|
||||
import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker.js';
|
||||
import { getBlotTextPartitions, matchBlotTextPartitions } from '../util.js';
|
||||
import { handleOutsideClick } from '../../util/handleOutsideClick.js';
|
||||
import { createLogger } from '../../logging/log.js';
|
||||
import { FunStaticEmoji } from '../../components/fun/FunEmoji.js';
|
||||
import type { EmojiParentKey } from '../../components/fun/data/emojis.js';
|
||||
import {
|
||||
EmojiSkinTone,
|
||||
getEmojiParentByKey,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
normalizeShortNameCompletionDisplay,
|
||||
} from '../../components/fun/data/emojis.js';
|
||||
@@ -27,13 +25,14 @@ import type {
|
||||
FunEmojiSearch,
|
||||
} from '../../components/fun/useFunEmojiSearch.js';
|
||||
import { type FunEmojiLocalizer } from '../../components/fun/useFunEmojiLocalizer.js';
|
||||
import type { FunEmojiSelection } from '../../components/fun/panels/FunPanelEmojis.js';
|
||||
|
||||
const { isNumber, debounce } = lodash;
|
||||
|
||||
const log = createLogger('completion');
|
||||
|
||||
export type EmojiCompletionOptions = {
|
||||
onPickEmoji: (emoji: EmojiPickDataType) => void;
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
|
||||
setEmojiPickerElement: (element: JSX.Element | null) => void;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSearch: FunEmojiSearch;
|
||||
@@ -41,7 +40,7 @@ export type EmojiCompletionOptions = {
|
||||
};
|
||||
|
||||
export type InsertEmojiOptionsType = Readonly<{
|
||||
shortName: string;
|
||||
emojiParentKey: EmojiParentKey;
|
||||
index: number;
|
||||
range: number;
|
||||
withTrailingSpace?: boolean;
|
||||
@@ -173,10 +172,9 @@ export class EmojiCompletion {
|
||||
this.options.emojiLocalizer.getParentKeyForText(leftTokenText);
|
||||
if (parentKey != null) {
|
||||
const numberOfColons = isSelfClosing ? 2 : 1;
|
||||
const emoji = getEmojiParentByKey(parentKey);
|
||||
|
||||
this.insertEmoji({
|
||||
shortName: emoji.englishShortNameDefault,
|
||||
emojiParentKey: parentKey,
|
||||
index: range.index - leftTokenText.length - numberOfColons,
|
||||
range: leftTokenText.length + numberOfColons,
|
||||
justPressedColon,
|
||||
@@ -194,9 +192,8 @@ export class EmojiCompletion {
|
||||
this.options.emojiLocalizer.getParentKeyForText(tokenText);
|
||||
|
||||
if (parentKey != null) {
|
||||
const emoji = getEmojiParentByKey(parentKey);
|
||||
this.insertEmoji({
|
||||
shortName: emoji.englishShortNameDefault,
|
||||
emojiParentKey: parentKey,
|
||||
index: range.index - leftTokenText.length - 1,
|
||||
range: tokenText.length + 2,
|
||||
justPressedColon,
|
||||
@@ -252,10 +249,9 @@ export class EmojiCompletion {
|
||||
}
|
||||
|
||||
const [, tokenText] = tokenTextMatch;
|
||||
const parent = getEmojiParentByKey(result.parentKey);
|
||||
|
||||
this.insertEmoji({
|
||||
shortName: parent.englishShortNameDefault,
|
||||
emojiParentKey: result.parentKey,
|
||||
index: range.index - tokenText.length - 1,
|
||||
range: tokenText.length + 1,
|
||||
withTrailingSpace: true,
|
||||
@@ -263,15 +259,16 @@ export class EmojiCompletion {
|
||||
}
|
||||
|
||||
insertEmoji({
|
||||
shortName,
|
||||
emojiParentKey,
|
||||
index,
|
||||
range,
|
||||
withTrailingSpace = false,
|
||||
justPressedColon = false,
|
||||
}: InsertEmojiOptionsType): void {
|
||||
const emoji = convertShortName(
|
||||
shortName,
|
||||
this.options.emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
const skinTone = this.options.emojiSkinToneDefault ?? EmojiSkinTone.None;
|
||||
const emojiVariant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
emojiParentKey,
|
||||
skinTone
|
||||
);
|
||||
|
||||
let source = this.quill.getText(index, range);
|
||||
@@ -283,7 +280,7 @@ export class EmojiCompletion {
|
||||
.retain(index)
|
||||
.delete(range)
|
||||
.insert({
|
||||
emoji: { value: emoji, source },
|
||||
emoji: { value: emojiVariant.value, source },
|
||||
});
|
||||
|
||||
if (withTrailingSpace) {
|
||||
@@ -296,9 +293,8 @@ export class EmojiCompletion {
|
||||
this.quill.setSelection(index + 1, 0, 'user');
|
||||
}
|
||||
|
||||
this.options.onPickEmoji({
|
||||
shortName,
|
||||
skinTone: this.options.emojiSkinToneDefault ?? EmojiSkinTone.None,
|
||||
this.options.onSelectEmoji({
|
||||
variantKey: emojiVariant.key,
|
||||
});
|
||||
|
||||
this.reset();
|
||||
|
||||
+12
-6
@@ -16,7 +16,11 @@ import type { EmojiBlot } from './emoji/blot.js';
|
||||
import { isNewlineOnlyOp, QuillFormattingStyle } from './formatting/menu.js';
|
||||
import { isNotNil } from '../util/isNotNil.js';
|
||||
import type { AciString } from '../types/ServiceId.js';
|
||||
import { emojiToData } from '../components/emoji/lib.js';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../components/fun/data/emojis.js';
|
||||
|
||||
export type Matcher = (
|
||||
node: HTMLElement,
|
||||
@@ -458,15 +462,17 @@ export const insertEmojiOps = (
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((match = re.exec(text))) {
|
||||
const [emoji] = match;
|
||||
const emojiData = emojiToData(emoji);
|
||||
if (emojiData) {
|
||||
const [emojiMatch] = match;
|
||||
if (isEmojiVariantValue(emojiMatch)) {
|
||||
const variantKey = getEmojiVariantKeyByValue(emojiMatch);
|
||||
const variant = getEmojiVariantByKey(variantKey);
|
||||
|
||||
ops.push({ insert: text.slice(index, match.index), attributes });
|
||||
ops.push({
|
||||
insert: { emoji: { value: emoji } },
|
||||
insert: { emoji: { value: variant.value } },
|
||||
attributes: { ...existingAttributes, ...attributes },
|
||||
});
|
||||
index = match.index + emoji.length;
|
||||
index = match.index + variant.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export const DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES = [
|
||||
'heart',
|
||||
'thumbsup',
|
||||
'thumbsdown',
|
||||
'joy',
|
||||
'open_mouth',
|
||||
'cry',
|
||||
import { EMOJI_PARENT_KEY_CONSTANTS } from '../components/fun/data/emojis.js';
|
||||
|
||||
export const DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS = [
|
||||
EMOJI_PARENT_KEY_CONSTANTS.RED_HEART,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.THUMBS_DOWN,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_TEARS_OF_JOY,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_OPEN_MOUTH,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.CRYING_FACE,
|
||||
];
|
||||
|
||||
// This is used in storybook for simplicity. Normally we prefer to convert emoji short
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
import lodash from 'lodash';
|
||||
import { createLogger } from '../logging/log.js';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from './constants.js';
|
||||
import { convertShortName } from '../components/emoji/lib.js';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS } from './constants.js';
|
||||
import { isValidReactionEmoji } from './isValidReactionEmoji.js';
|
||||
import type { EmojiSkinTone } from '../components/fun/data/emojis.js';
|
||||
import {
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
type EmojiSkinTone,
|
||||
} from '../components/fun/data/emojis.js';
|
||||
|
||||
const { times } = lodash;
|
||||
|
||||
@@ -16,7 +18,7 @@ const MAX_STORED_LENGTH = 20;
|
||||
const MAX_ITEM_LENGTH = 20;
|
||||
|
||||
const PREFERRED_REACTION_EMOJI_COUNT =
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.length;
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS.length;
|
||||
|
||||
export function getPreferredReactionEmoji(
|
||||
storedValue: unknown,
|
||||
@@ -32,27 +34,27 @@ export function getPreferredReactionEmoji(
|
||||
return storedItem;
|
||||
}
|
||||
|
||||
const fallbackShortName: undefined | string =
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES[index];
|
||||
if (!fallbackShortName) {
|
||||
const fallbackParentKey =
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS.at(index);
|
||||
if (fallbackParentKey == null) {
|
||||
log.error(
|
||||
'Index is out of range. Is the preferred count larger than the list of fallbacks?'
|
||||
);
|
||||
return '❤️';
|
||||
}
|
||||
|
||||
const fallbackEmoji = convertShortName(
|
||||
fallbackShortName,
|
||||
const fallbackEmoji = getEmojiVariantByParentKeyAndSkinTone(
|
||||
fallbackParentKey,
|
||||
emojiSkinToneDefault
|
||||
);
|
||||
if (!fallbackEmoji) {
|
||||
if (fallbackEmoji == null) {
|
||||
log.error(
|
||||
'No fallback emoji. Does the fallback list contain an invalid short name?'
|
||||
);
|
||||
return '❤️';
|
||||
}
|
||||
|
||||
return fallbackEmoji;
|
||||
return fallbackEmoji.value;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
import lodash from 'lodash';
|
||||
import type { ThunkAction } from 'redux-thunk';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker.js';
|
||||
import { DataWriter } from '../../sql/Client.js';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.js';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions.js';
|
||||
import type { FunEmojiSelection } from '../../components/fun/panels/FunPanelEmojis.js';
|
||||
import {
|
||||
getEmojiParentByKey,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
} from '../../components/fun/data/emojis.js';
|
||||
|
||||
const { take, uniq } = lodash;
|
||||
|
||||
@@ -39,11 +43,16 @@ export const useEmojisActions = (): BoundActionCreatorsMapObject<
|
||||
typeof actions
|
||||
> => useBoundActions(actions);
|
||||
|
||||
function onUseEmoji({
|
||||
shortName,
|
||||
}: EmojiPickDataType): ThunkAction<void, unknown, unknown, UseEmojiAction> {
|
||||
function onUseEmoji(
|
||||
emojiSelection: FunEmojiSelection
|
||||
): ThunkAction<void, unknown, unknown, UseEmojiAction> {
|
||||
return async dispatch => {
|
||||
try {
|
||||
const emojiParentKey = getEmojiParentKeyByVariantKey(
|
||||
emojiSelection.variantKey
|
||||
);
|
||||
const emojiParent = getEmojiParentByKey(emojiParentKey);
|
||||
const shortName = emojiParent.englishShortNameDefault;
|
||||
await updateEmojiUsage(shortName);
|
||||
dispatch(useEmoji(shortName));
|
||||
} catch (err) {
|
||||
|
||||
@@ -10,11 +10,13 @@ import { replaceIndex } from '../../util/replaceIndex.js';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.js';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions.js';
|
||||
import type { StateType as RootStateType } from '../reducer.js';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../../reactions/constants.js';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS } from '../../reactions/constants.js';
|
||||
import { getPreferredReactionEmoji } from '../../reactions/preferredReactionEmoji.js';
|
||||
import { getEmojiSkinToneDefault } from '../selectors/items.js';
|
||||
import { convertShortName } from '../../components/emoji/lib.js';
|
||||
import { EmojiSkinTone } from '../../components/fun/data/emojis.js';
|
||||
import {
|
||||
EmojiSkinTone,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
} from '../../components/fun/data/emojis.js';
|
||||
|
||||
const { omit } = lodash;
|
||||
|
||||
@@ -295,9 +297,13 @@ export function reducer(
|
||||
customizePreferredReactionsModal: {
|
||||
...state.customizePreferredReactionsModal,
|
||||
draftPreferredReactions:
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(shortName =>
|
||||
convertShortName(shortName, emojiSkinTone)
|
||||
),
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS.map(parentKey => {
|
||||
const variant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
parentKey,
|
||||
emojiSkinTone
|
||||
);
|
||||
return variant.value;
|
||||
}),
|
||||
selectedDraftEmojiIndex: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
import { getConversationSelector, getMe } from '../selectors/conversations.js';
|
||||
import { getIntl, getUserACI } from '../selectors/user.js';
|
||||
import { SmartCallingDeviceSelection } from './CallingDeviceSelection.js';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker.js';
|
||||
import { renderReactionPicker } from './renderReactionPicker.js';
|
||||
import { isSharingPhoneNumberWithEverybody as getIsSharingPhoneNumberWithEverybody } from '../../util/phoneNumberSharingMode.js';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals.js';
|
||||
@@ -465,7 +464,6 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
||||
playRingtone={playRingtone}
|
||||
removeClient={removeClient}
|
||||
renderDeviceSelection={renderDeviceSelection}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
renderReactionPicker={renderReactionPicker}
|
||||
ringingCall={ringingCall}
|
||||
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
||||
|
||||
@@ -30,51 +30,34 @@ import {
|
||||
getSelectedMessageIds,
|
||||
isMissingRequiredProfileSharing,
|
||||
} from '../selectors/conversations.js';
|
||||
import { selectRecentEmojis } from '../selectors/emojis.js';
|
||||
import {
|
||||
getDefaultConversationColor,
|
||||
getEmojiSkinToneDefault,
|
||||
getShowStickerPickerHint,
|
||||
getShowStickersIntroduction,
|
||||
getTextFormattingEnabled,
|
||||
} from '../selectors/items.js';
|
||||
import { canForward, getPropsForQuote } from '../selectors/message.js';
|
||||
import {
|
||||
getBlessedStickerPacks,
|
||||
getInstalledStickerPacks,
|
||||
getKnownStickerPacks,
|
||||
getReceivedStickerPacks,
|
||||
getRecentStickers,
|
||||
getRecentlyInstalledStickerPack,
|
||||
} from '../selectors/stickers.js';
|
||||
import {
|
||||
getIntl,
|
||||
getPlatform,
|
||||
getTheme,
|
||||
getUserConversationId,
|
||||
} from '../selectors/user.js';
|
||||
import type { SmartCompositionRecordingProps } from './CompositionRecording.js';
|
||||
import { SmartCompositionRecording } from './CompositionRecording.js';
|
||||
import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft.js';
|
||||
import { SmartCompositionRecordingDraft } from './CompositionRecordingDraft.js';
|
||||
import { useItemsActions } from '../ducks/items.js';
|
||||
import { useComposerActions } from '../ducks/composer.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.js';
|
||||
import { useAudioRecorderActions } from '../ducks/audioRecorder.js';
|
||||
import { useEmojisActions } from '../ducks/emojis.js';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals.js';
|
||||
import { useStickersActions } from '../ducks/stickers.js';
|
||||
import { useToastActions } from '../ducks/toast.js';
|
||||
import { isShowingAnyModal } from '../selectors/globalModals.js';
|
||||
import { isConversationEverUnregistered } from '../../util/isConversationUnregistered.js';
|
||||
import { isDirectConversation } from '../../util/whatTypeOfConversation.js';
|
||||
import { isConversationMuted } from '../../util/isConversationMuted.js';
|
||||
import type { EmojiSkinTone } from '../../components/fun/data/emojis.js';
|
||||
|
||||
function renderSmartCompositionRecording(
|
||||
recProps: SmartCompositionRecordingProps
|
||||
) {
|
||||
return <SmartCompositionRecording {...recProps} />;
|
||||
function renderSmartCompositionRecording() {
|
||||
return <SmartCompositionRecording />;
|
||||
}
|
||||
|
||||
function renderSmartCompositionRecordingDraft(
|
||||
@@ -95,21 +78,12 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault);
|
||||
const recentEmojis = useSelector(selectRecentEmojis);
|
||||
const selectedMessageIds = useSelector(getSelectedMessageIds);
|
||||
const messageLookup = useSelector(getMessages);
|
||||
const isFormattingEnabled = useSelector(getTextFormattingEnabled);
|
||||
const lastEditableMessageId = useSelector(getLastEditableMessageId);
|
||||
const receivedPacks = useSelector(getReceivedStickerPacks);
|
||||
const installedPacks = useSelector(getInstalledStickerPacks);
|
||||
const blessedPacks = useSelector(getBlessedStickerPacks);
|
||||
const knownPacks = useSelector(getKnownStickerPacks);
|
||||
const platform = useSelector(getPlatform);
|
||||
const shouldHidePopovers = useSelector(getHasPanelOpen);
|
||||
const installedPack = useSelector(getRecentlyInstalledStickerPack);
|
||||
const recentStickers = useSelector(getRecentStickers);
|
||||
const showStickersIntroduction = useSelector(getShowStickersIntroduction);
|
||||
const showStickerPickerHint = useSelector(getShowStickerPickerHint);
|
||||
const recordingState = useSelector(getRecordingState);
|
||||
const errorDialogAudioRecorderType = useSelector(
|
||||
getErrorDialogAudioRecorderType
|
||||
@@ -205,23 +179,6 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
defaultConversationColor,
|
||||
]);
|
||||
|
||||
const { putItem, removeItem } = useItemsActions();
|
||||
|
||||
const onEmojiSkinToneDefaultChange = useCallback(
|
||||
(emojiSkinTone: EmojiSkinTone) => {
|
||||
putItem('emojiSkinToneDefault', emojiSkinTone);
|
||||
},
|
||||
[putItem]
|
||||
);
|
||||
|
||||
const clearShowIntroduction = useCallback(() => {
|
||||
removeItem('showStickersIntroduction');
|
||||
}, [removeItem]);
|
||||
|
||||
const clearShowPickerHint = useCallback(() => {
|
||||
removeItem('showStickerPickerHint');
|
||||
}, [removeItem]);
|
||||
|
||||
const {
|
||||
onTextTooLong,
|
||||
onCloseLinkPreview,
|
||||
@@ -259,7 +216,6 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
toggleForwardMessagesModal,
|
||||
toggleDraftGifMessageSendModal,
|
||||
} = useGlobalModalActions();
|
||||
const { clearInstalledStickerPack } = useStickersActions();
|
||||
const { showToast } = useToastActions();
|
||||
const { onEditorStateChange } = useComposerActions();
|
||||
|
||||
@@ -318,19 +274,9 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
quotedMessageAuthorAci={quotedMessage?.quote?.authorAci ?? null}
|
||||
quotedMessageSentAt={quotedMessage?.quote?.id ?? null}
|
||||
setQuoteByMessageId={setQuoteByMessageId}
|
||||
// Emojis
|
||||
recentEmojis={recentEmojis}
|
||||
// Fun Picker
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onPickEmoji={onUseEmoji}
|
||||
// Stickers
|
||||
receivedPacks={receivedPacks}
|
||||
installedPack={installedPack}
|
||||
blessedPacks={blessedPacks}
|
||||
knownPacks={knownPacks}
|
||||
installedPacks={installedPacks}
|
||||
recentStickers={recentStickers}
|
||||
showIntroduction={showStickersIntroduction}
|
||||
showPickerHint={showStickerPickerHint}
|
||||
onSelectEmoji={onUseEmoji}
|
||||
// Message Requests
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest ?? null}
|
||||
removalStage={conversation.removalStage ?? null}
|
||||
@@ -385,10 +331,6 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
// DraftGifMessageSendModal
|
||||
toggleDraftGifMessageSendModal={toggleDraftGifMessageSendModal}
|
||||
// Dispatch
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
clearShowIntroduction={clearShowIntroduction}
|
||||
clearInstalledStickerPack={clearInstalledStickerPack}
|
||||
clearShowPickerHint={clearShowPickerHint}
|
||||
showToast={showToast}
|
||||
sendStickerMessage={sendStickerMessage}
|
||||
sendEditedMessage={sendEditedMessage}
|
||||
|
||||
@@ -10,14 +10,8 @@ import { useToastActions } from '../ducks/toast.js';
|
||||
import { getSelectedConversationId } from '../selectors/conversations.js';
|
||||
import { getIntl } from '../selectors/user.js';
|
||||
|
||||
export type SmartCompositionRecordingProps = {
|
||||
onBeforeSend: () => void;
|
||||
};
|
||||
|
||||
export const SmartCompositionRecording = memo(
|
||||
function SmartCompositionRecording({
|
||||
onBeforeSend,
|
||||
}: SmartCompositionRecordingProps) {
|
||||
function SmartCompositionRecording() {
|
||||
const i18n = useSelector(getIntl);
|
||||
const selectedConversationId = useSelector(getSelectedConversationId);
|
||||
const { errorRecording, cancelRecording, completeRecording } =
|
||||
@@ -34,18 +28,12 @@ export const SmartCompositionRecording = memo(
|
||||
const handleSend = useCallback(() => {
|
||||
if (selectedConversationId) {
|
||||
completeRecording(selectedConversationId, voiceNoteAttachment => {
|
||||
onBeforeSend();
|
||||
sendMultiMediaMessage(selectedConversationId, {
|
||||
voiceNoteAttachment,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [
|
||||
selectedConversationId,
|
||||
completeRecording,
|
||||
onBeforeSend,
|
||||
sendMultiMediaMessage,
|
||||
]);
|
||||
}, [selectedConversationId, completeRecording, sendMultiMediaMessage]);
|
||||
|
||||
if (!selectedConversationId) {
|
||||
return null;
|
||||
|
||||
@@ -38,7 +38,7 @@ export const SmartCompositionTextArea = memo(function SmartCompositionTextArea(
|
||||
const platform = useSelector(getPlatform);
|
||||
const ourConversationId = useSelector(getUserConversationId);
|
||||
|
||||
const { onUseEmoji: onPickEmoji } = useEmojiActions();
|
||||
const { onUseEmoji } = useEmojiActions();
|
||||
const { setEmojiSkinToneDefault } = useItemsActions();
|
||||
const { onTextTooLong } = useComposerActions();
|
||||
|
||||
@@ -53,7 +53,7 @@ export const SmartCompositionTextArea = memo(function SmartCompositionTextArea(
|
||||
i18n={i18n}
|
||||
isActive
|
||||
isFormattingEnabled={isFormattingEnabled}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSelectEmoji={onUseEmoji}
|
||||
onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault}
|
||||
onTextTooLong={onTextTooLong}
|
||||
platform={platform}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, forwardRef, memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useRecentEmojis } from '../selectors/emojis.js';
|
||||
import { useEmojisActions as useEmojiActions } from '../ducks/emojis.js';
|
||||
import type {
|
||||
EmojiPickDataType,
|
||||
Props as EmojiPickerProps,
|
||||
} from '../../components/emoji/EmojiPicker.js';
|
||||
import { EmojiPicker } from '../../components/emoji/EmojiPicker.js';
|
||||
import { getIntl } from '../selectors/user.js';
|
||||
import { getEmojiSkinToneDefault } from '../selectors/items.js';
|
||||
import { EmojiSkinTone } from '../../components/fun/data/emojis.js';
|
||||
|
||||
export const SmartEmojiPicker = memo(
|
||||
forwardRef<
|
||||
HTMLDivElement,
|
||||
Pick<
|
||||
EmojiPickerProps,
|
||||
| 'onClickSettings'
|
||||
| 'onPickEmoji'
|
||||
| 'onEmojiSkinToneDefaultChange'
|
||||
| 'onClose'
|
||||
| 'style'
|
||||
>
|
||||
>(function SmartEmojiPickerInner(
|
||||
{
|
||||
onClickSettings,
|
||||
onPickEmoji,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
onClose,
|
||||
style,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault);
|
||||
|
||||
const recentEmojis = useRecentEmojis();
|
||||
const { onUseEmoji } = useEmojiActions();
|
||||
|
||||
const handlePickEmoji = useCallback(
|
||||
(data: EmojiPickDataType) => {
|
||||
onUseEmoji({ shortName: data.shortName, skinTone: EmojiSkinTone.None });
|
||||
onPickEmoji(data);
|
||||
},
|
||||
[onUseEmoji, onPickEmoji]
|
||||
);
|
||||
|
||||
return (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
onClickSettings={onClickSettings}
|
||||
onClose={onClose}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
onPickEmoji={handlePickEmoji}
|
||||
recentEmojis={recentEmojis}
|
||||
ref={ref}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
style={style}
|
||||
wasInvokedFromKeyboard={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
@@ -83,10 +83,7 @@ export const SmartFunProvider = memo(function SmartFunProvider(
|
||||
|
||||
const handleSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
onUseEmoji({
|
||||
shortName: emojiSelection.englishShortName,
|
||||
skinTone: emojiSelection.skinTone,
|
||||
});
|
||||
onUseEmoji(emojiSelection);
|
||||
},
|
||||
[onUseEmoji]
|
||||
);
|
||||
|
||||
@@ -7,11 +7,9 @@ import type { MutableRefObject } from 'react';
|
||||
|
||||
import { ProfileEditor } from '../../components/ProfileEditor.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.js';
|
||||
import { useItemsActions } from '../ducks/items.js';
|
||||
import { useToastActions } from '../ducks/toast.js';
|
||||
import { useUsernameActions } from '../ducks/username.js';
|
||||
import { getMe, getProfileUpdateError } from '../selectors/conversations.js';
|
||||
import { selectRecentEmojis } from '../selectors/emojis.js';
|
||||
import {
|
||||
getEmojiSkinToneDefault,
|
||||
getHasCompletedUsernameLinkOnboarding,
|
||||
@@ -58,7 +56,6 @@ export const SmartProfileEditor = memo(function SmartProfileEditor(props: {
|
||||
getHasCompletedUsernameLinkOnboarding
|
||||
);
|
||||
const hasError = useSelector(getProfileUpdateError);
|
||||
const recentEmojis = useSelector(selectRecentEmojis);
|
||||
const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault);
|
||||
const usernameCorrupted = useSelector(getUsernameCorrupted);
|
||||
const usernameEditState = useSelector(getUsernameEditState);
|
||||
@@ -84,7 +81,6 @@ export const SmartProfileEditor = memo(function SmartProfileEditor(props: {
|
||||
deleteUsername,
|
||||
} = useUsernameActions();
|
||||
const { showToast } = useToastActions();
|
||||
const { setEmojiSkinToneDefault } = useItemsActions();
|
||||
const { changeLocation } = useNavActions();
|
||||
|
||||
let errorDialog: JSX.Element | undefined;
|
||||
@@ -139,10 +135,8 @@ export const SmartProfileEditor = memo(function SmartProfileEditor(props: {
|
||||
markCompletedUsernameLinkOnboarding
|
||||
}
|
||||
onProfileChanged={myProfileChanged}
|
||||
onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault}
|
||||
openUsernameReservationModal={openUsernameReservationModal}
|
||||
profileAvatarUrl={profileAvatarUrl}
|
||||
recentEmojis={recentEmojis}
|
||||
renderUsernameEditor={renderUsernameEditor}
|
||||
replaceAvatar={replaceAvatar}
|
||||
resetUsernameLink={resetUsernameLink}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
import type { Ref } from 'react';
|
||||
import React, { forwardRef, memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { usePreferredReactionsActions } from '../ducks/preferredReactions.js';
|
||||
import { useItemsActions } from '../ducks/items.js';
|
||||
import { getIntl } from '../selectors/user.js';
|
||||
import { getPreferredReactionEmoji } from '../selectors/items.js';
|
||||
import type { Props as InternalProps } from '../../components/conversation/ReactionPicker.js';
|
||||
@@ -26,21 +24,12 @@ export const SmartReactionPicker = memo(
|
||||
props: ExternalProps,
|
||||
ref: Ref<HTMLDivElement>
|
||||
) {
|
||||
const { openCustomizePreferredReactionsModal } =
|
||||
usePreferredReactionsActions();
|
||||
|
||||
const { setEmojiSkinToneDefault } = useItemsActions();
|
||||
|
||||
const i18n = useSelector(getIntl);
|
||||
const preferredReactionEmoji = useSelector(getPreferredReactionEmoji);
|
||||
|
||||
return (
|
||||
<ReactionPicker
|
||||
i18n={i18n}
|
||||
onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault}
|
||||
openCustomizePreferredReactionsModal={
|
||||
openCustomizePreferredReactionsModal
|
||||
}
|
||||
preferredReactionEmoji={preferredReactionEmoji}
|
||||
ref={ref}
|
||||
{...props}
|
||||
|
||||
@@ -1,43 +1,20 @@
|
||||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ShortcutGuideModal } from '../../components/ShortcutGuideModal.js';
|
||||
import { countStickers } from '../../components/stickers/lib.js';
|
||||
import { getIntl, getPlatform } from '../selectors/user.js';
|
||||
import {
|
||||
getBlessedStickerPacks,
|
||||
getInstalledStickerPacks,
|
||||
getKnownStickerPacks,
|
||||
getReceivedStickerPacks,
|
||||
} from '../selectors/stickers.js';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals.js';
|
||||
|
||||
export const SmartShortcutGuideModal = memo(function SmartShortcutGuideModal() {
|
||||
const i18n = useSelector(getIntl);
|
||||
const blessedPacks = useSelector(getBlessedStickerPacks);
|
||||
const installedPacks = useSelector(getInstalledStickerPacks);
|
||||
const knownPacks = useSelector(getKnownStickerPacks);
|
||||
const receivedPacks = useSelector(getReceivedStickerPacks);
|
||||
const platform = useSelector(getPlatform);
|
||||
|
||||
const { closeShortcutGuideModal } = useGlobalModalActions();
|
||||
|
||||
const hasInstalledStickers = useMemo(() => {
|
||||
return (
|
||||
countStickers({
|
||||
knownPacks,
|
||||
blessedPacks,
|
||||
installedPacks,
|
||||
receivedPacks,
|
||||
}) > 0
|
||||
);
|
||||
}, [blessedPacks, installedPacks, knownPacks, receivedPacks]);
|
||||
|
||||
return (
|
||||
<ShortcutGuideModal
|
||||
hasInstalledStickers={hasInstalledStickers}
|
||||
platform={platform}
|
||||
closeShortcutGuideModal={closeShortcutGuideModal}
|
||||
i18n={i18n}
|
||||
|
||||
@@ -20,10 +20,6 @@ import {
|
||||
getPlatform,
|
||||
getUserConversationId,
|
||||
} from '../selectors/user.js';
|
||||
import {
|
||||
getInstalledStickerPacks,
|
||||
getRecentStickers,
|
||||
} from '../selectors/stickers.js';
|
||||
import { getAddStoryData } from '../selectors/stories.js';
|
||||
import { getLinkPreview } from '../selectors/linkPreviews.js';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges.js';
|
||||
@@ -39,9 +35,7 @@ import { useAudioPlayerActions } from '../ducks/audioPlayer.js';
|
||||
import { useComposerActions } from '../ducks/composer.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.js';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals.js';
|
||||
import { useItemsActions } from '../ducks/items.js';
|
||||
import { useLinkPreviewActions } from '../ducks/linkPreviews.js';
|
||||
import { useRecentEmojis } from '../selectors/emojis.js';
|
||||
import { useStoriesActions } from '../ducks/stories.js';
|
||||
import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists.js';
|
||||
|
||||
@@ -78,10 +72,8 @@ export const SmartStoryCreator = memo(function SmartStoryCreator() {
|
||||
const groupStories = useSelector(getGroupStories);
|
||||
const hasSetMyStoriesPrivacy = useSelector(getHasSetMyStoriesPrivacy);
|
||||
const i18n = useSelector(getIntl);
|
||||
const installedPacks = useSelector(getInstalledStickerPacks);
|
||||
const linkPreviewForSource = useSelector(getLinkPreview);
|
||||
const me = useSelector(getMe);
|
||||
const recentStickers = useSelector(getRecentStickers);
|
||||
const signalConnections = useSelector(getAllSignalConnections);
|
||||
const mostRecentActiveStoryTimestampByGroupOrDistributionList = useSelector(
|
||||
selectMostRecentActiveStoryTimestampByGroupOrDistributionList
|
||||
@@ -97,13 +89,10 @@ export const SmartStoryCreator = memo(function SmartStoryCreator() {
|
||||
file = addStoryData.file as File;
|
||||
}
|
||||
|
||||
const recentEmojis = useRecentEmojis();
|
||||
const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault);
|
||||
const { setEmojiSkinToneDefault } = useItemsActions();
|
||||
const { onUseEmoji } = useEmojisActions();
|
||||
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
|
||||
const { onTextTooLong } = useComposerActions();
|
||||
const { onUseEmoji: onPickEmoji } = useEmojisActions();
|
||||
|
||||
const isFormattingEnabled = useSelector(getTextFormattingEnabled);
|
||||
const platform = useSelector(getPlatform);
|
||||
@@ -124,7 +113,6 @@ export const SmartStoryCreator = memo(function SmartStoryCreator() {
|
||||
hasFirstStoryPostExperience={!hasSetMyStoriesPrivacy}
|
||||
i18n={i18n}
|
||||
imageToBlurHash={imageToBlurHash}
|
||||
installedPacks={installedPacks}
|
||||
isFormattingEnabled={isFormattingEnabled}
|
||||
isSending={isSending}
|
||||
linkPreview={linkPreview}
|
||||
@@ -137,20 +125,16 @@ export const SmartStoryCreator = memo(function SmartStoryCreator() {
|
||||
onDistributionListCreated={createDistributionList}
|
||||
onHideMyStoriesFrom={hideMyStoriesFrom}
|
||||
onMediaPlaybackStart={pauseVoiceNotePlayer}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSelectEmoji={onUseEmoji}
|
||||
onRemoveMembers={removeMembersFromDistributionList}
|
||||
onRepliesNReactionsChanged={allowsRepliesChanged}
|
||||
onSelectedStoryList={verifyStoryListMembers}
|
||||
onSend={sendStoryMessage}
|
||||
onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault}
|
||||
onTextTooLong={onTextTooLong}
|
||||
onUseEmoji={onUseEmoji}
|
||||
onViewersUpdated={updateStoryViewers}
|
||||
ourConversationId={ourConversationId}
|
||||
platform={platform}
|
||||
processAttachment={processAttachment}
|
||||
recentEmojis={recentEmojis}
|
||||
recentStickers={recentStickers}
|
||||
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
|
||||
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
|
||||
signalConnections={signalConnections}
|
||||
|
||||
@@ -28,13 +28,10 @@ import {
|
||||
} from '../selectors/stories.js';
|
||||
import { isInFullScreenCall } from '../selectors/calling.js';
|
||||
import { isSignalConversation as getIsSignalConversation } from '../../util/isSignalConversation.js';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker.js';
|
||||
import { strictAssert } from '../../util/assert.js';
|
||||
import { asyncShouldNeverBeCalled } from '../../util/shouldNeverBeCalled.js';
|
||||
import { useEmojisActions } from '../ducks/emojis.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.js';
|
||||
import { useRecentEmojis } from '../selectors/emojis.js';
|
||||
import { useItemsActions } from '../ducks/items.js';
|
||||
import { useAudioPlayerActions } from '../ducks/audioPlayer.js';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals.js';
|
||||
import { useStoriesActions } from '../ducks/stories.js';
|
||||
@@ -62,7 +59,6 @@ export const SmartStoryViewer = memo(function SmartStoryViewer() {
|
||||
showConversation,
|
||||
toggleHideStories,
|
||||
} = useConversationsActions();
|
||||
const { setEmojiSkinToneDefault } = useItemsActions();
|
||||
const { showToast } = useToastActions();
|
||||
const { showContactModal } = useGlobalModalActions();
|
||||
|
||||
@@ -81,7 +77,6 @@ export const SmartStoryViewer = memo(function SmartStoryViewer() {
|
||||
const conversationSelector = useSelector(getConversationSelector);
|
||||
|
||||
const getStoryById = useSelector(getStoryByIdSelector);
|
||||
const recentEmojis = useRecentEmojis();
|
||||
const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault);
|
||||
const replyState = useSelector(getStoryReplies);
|
||||
const hasAllStoriesUnmuted = useSelector(getHasAllStoriesUnmuted);
|
||||
@@ -163,15 +158,12 @@ export const SmartStoryViewer = memo(function SmartStoryViewer() {
|
||||
onMediaPlaybackStart={pauseVoiceNotePlayer}
|
||||
onReactToStory={handleReactToStory}
|
||||
onReplyToStory={handleReplyToStory}
|
||||
onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault}
|
||||
onTextTooLong={handleTextTooLong}
|
||||
onUseEmoji={onUseEmoji}
|
||||
onSelectEmoji={onUseEmoji}
|
||||
ourConversationId={ourConversationId}
|
||||
platform={platform}
|
||||
preferredReactionEmoji={preferredReactionEmoji}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
recentEmojis={recentEmojis}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
replyState={replyState}
|
||||
retryMessageSend={retryMessageSend}
|
||||
saveAttachment={internalUser ? saveAttachment : asyncShouldNeverBeCalled}
|
||||
|
||||
@@ -33,7 +33,6 @@ import { SmartContactName } from './ContactName.js';
|
||||
import { SmartUniversalTimerNotification } from './UniversalTimerNotification.js';
|
||||
import { isSameDay } from '../../util/timestamp.js';
|
||||
import { renderAudioAttachment } from './renderAudioAttachment.js';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker.js';
|
||||
import { renderReactionPicker } from './renderReactionPicker.js';
|
||||
import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation.js';
|
||||
|
||||
@@ -188,7 +187,6 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
||||
isTargeted={isTargeted}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
renderContact={renderContact}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
renderReactionPicker={renderReactionPicker}
|
||||
renderUniversalTimerNotification={renderUniversalTimerNotification}
|
||||
shouldCollapseAbove={shouldCollapseAbove}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { RenderEmojiPickerProps } from '../../components/conversation/ReactionPicker.js';
|
||||
import { SmartEmojiPicker } from './EmojiPicker.js';
|
||||
|
||||
export function renderEmojiPicker({
|
||||
ref,
|
||||
onClickSettings,
|
||||
onPickEmoji,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
onClose,
|
||||
style,
|
||||
}: RenderEmojiPickerProps): JSX.Element {
|
||||
return (
|
||||
<SmartEmojiPicker
|
||||
ref={ref}
|
||||
onClickSettings={onClickSettings}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
onClose={onClose}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -10,9 +10,10 @@ import type {
|
||||
InsertEmojiOptionsType,
|
||||
} from '../../../quill/emoji/completion.js';
|
||||
import {
|
||||
EMOJI_VARIANT_KEY_CONSTANTS,
|
||||
EmojiSkinTone,
|
||||
emojiVariantConstant,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
} from '../../../components/fun/data/emojis.js';
|
||||
import {
|
||||
_createFunEmojiSearch,
|
||||
@@ -25,9 +26,13 @@ import {
|
||||
import type { LocaleEmojiListType } from '../../../types/emoji.js';
|
||||
|
||||
const EMOJI_VARIANTS = {
|
||||
SMILE: emojiVariantConstant('\u{1F604}'),
|
||||
SMILE_CAT: emojiVariantConstant('\u{1F638}'),
|
||||
FRIEND_SHRIMP: emojiVariantConstant('\u{1F364}'),
|
||||
SMILE: getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.GRINNING_FACE_WITH_SMILING_EYES
|
||||
),
|
||||
SMILE_CAT: getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.GRINNING_CAT_WITH_SMILING_EYES
|
||||
),
|
||||
FRIEND_SHRIMP: getEmojiVariantByKey(EMOJI_VARIANT_KEY_CONSTANTS.FRIED_SHRIMP),
|
||||
} as const;
|
||||
|
||||
const PARENT_KEYS = {
|
||||
@@ -82,7 +87,7 @@ describe('emojiCompletion', () => {
|
||||
const emojiLocalizer = _createFunEmojiLocalizer(localizerIndex);
|
||||
|
||||
const options: EmojiCompletionOptions = {
|
||||
onPickEmoji: sinon.stub(),
|
||||
onSelectEmoji: sinon.stub(),
|
||||
setEmojiPickerElement: sinon.stub(),
|
||||
emojiSkinToneDefault: EmojiSkinTone.None,
|
||||
emojiSearch,
|
||||
@@ -247,9 +252,9 @@ describe('emojiCompletion', () => {
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [{ shortName, index, range }] = insertEmojiStub.args[0];
|
||||
const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(shortName, 'smile');
|
||||
assert.equal(emojiParentKey, PARENT_KEYS.SMILE);
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, 7);
|
||||
});
|
||||
@@ -276,9 +281,9 @@ describe('emojiCompletion', () => {
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [{ shortName, index, range }] = insertEmojiStub.args[0];
|
||||
const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(shortName, 'smile');
|
||||
assert.equal(emojiParentKey, PARENT_KEYS.SMILE);
|
||||
assert.equal(index, 7);
|
||||
assert.equal(range, 7);
|
||||
});
|
||||
@@ -336,9 +341,9 @@ describe('emojiCompletion', () => {
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [{ shortName, index, range }] = insertEmojiStub.args[0];
|
||||
const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(shortName, 'smile');
|
||||
assert.equal(emojiParentKey, PARENT_KEYS.SMILE);
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, validEmoji.length);
|
||||
});
|
||||
@@ -385,9 +390,9 @@ describe('emojiCompletion', () => {
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [{ shortName, index, range }] = insertEmojiStub.args[0];
|
||||
const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(shortName, 'smile');
|
||||
assert.equal(emojiParentKey, PARENT_KEYS.SMILE);
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, 6);
|
||||
});
|
||||
@@ -430,10 +435,10 @@ describe('emojiCompletion', () => {
|
||||
});
|
||||
|
||||
it('inserts the currently selected emoji at the current cursor position', () => {
|
||||
const [{ shortName, index: insertIndex, range }] =
|
||||
const [{ emojiParentKey, index: insertIndex, range }] =
|
||||
insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(shortName, 'smile_cat');
|
||||
assert.equal(emojiParentKey, PARENT_KEYS.SMILE_CAT);
|
||||
assert.equal(insertIndex, 0);
|
||||
assert.equal(range, text.length);
|
||||
});
|
||||
|
||||
@@ -156,9 +156,23 @@ describe('storage service', function (this: Mocha.Suite) {
|
||||
}
|
||||
|
||||
debug('opening sticker manager');
|
||||
await conversationView
|
||||
.locator('.CompositionArea .module-sticker-button__button')
|
||||
.click();
|
||||
|
||||
const FunButton = window.getByRole('button', {
|
||||
name: 'Add an Emoji, Sticker, or GIF',
|
||||
});
|
||||
const FunDialog = window.getByRole('dialog', {
|
||||
name: 'Add an Emoji, Sticker, or GIF',
|
||||
});
|
||||
const FunPickerStickersTab = FunDialog.getByRole('tab', {
|
||||
name: 'Stickers',
|
||||
});
|
||||
const FunPickerAddSticker = FunDialog.getByRole('button', {
|
||||
name: 'Add a sticker pack',
|
||||
});
|
||||
|
||||
await FunButton.click();
|
||||
await FunPickerStickersTab.click();
|
||||
await FunPickerAddSticker.click();
|
||||
|
||||
const stickerManager = conversationView.locator(
|
||||
'[data-testid=StickerManager]'
|
||||
|
||||
@@ -1162,13 +1162,6 @@
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-23T00:07:11.885Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CompositionArea.tsx",
|
||||
"line": " const emojiButtonRef = useRef<EmojiButtonAPI | undefined>();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2022-07-07T20:51:44.602Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CompositionInput.tsx",
|
||||
@@ -1907,21 +1900,6 @@
|
||||
"updated": "2024-09-03T00:45:23.978Z",
|
||||
"reasonDetail": "Because we need the current tab value outside the callback"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/emoji/EmojiButton.tsx",
|
||||
"line": " const buttonRef = React.useRef<HTMLButtonElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2022-06-14T22:04:43.988Z",
|
||||
"reasonDetail": "Handling outside click"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/emoji/EmojiButton.tsx",
|
||||
"line": " const popperRef = React.useRef<HTMLDivElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-01-18T22:32:43.901Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/fun/FunGif.tsx",
|
||||
@@ -2118,14 +2096,6 @@
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2025-07-17T19:33:27.401Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/stickers/StickerButton.tsx",
|
||||
"line": " const buttonRef = React.useRef<HTMLButtonElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2022-06-14T22:04:43.988Z",
|
||||
"reasonDetail": "Handling outside click"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/hooks/useConfirmDiscard.tsx",
|
||||
|
||||
Reference in New Issue
Block a user