Remove old emoji and sticker pickers

This commit is contained in:
Jamie
2025-10-07 12:01:24 -07:00
committed by GitHub
parent 8b8f9a8f91
commit b73563ad9d
80 changed files with 533 additions and 4693 deletions
+4
View File
@@ -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"
+3
View File
@@ -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
-3
View File
@@ -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',
-1
View File
@@ -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'),
+1 -6
View File
@@ -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}
-1
View File
@@ -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'),
+6 -7
View File
@@ -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,
+4 -23
View File
@@ -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 {
+24 -153
View File
@@ -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}
+1 -1
View File
@@ -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',
+11 -12
View File
@@ -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,
+19 -47
View File
@@ -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"
+1 -4
View File
@@ -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
View File
@@ -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
-2
View File
@@ -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')}
+2 -3
View File
@@ -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}
/>
-7
View File
@@ -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}
/>
+24 -73
View File
@@ -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,
}));
}}
-6
View File
@@ -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} />;
}
+1 -9
View File
@@ -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>
+1 -4
View File
@@ -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}
/>
+1 -5
View File
@@ -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'
+29 -48
View File
@@ -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}
/>
)}
</>,
+1 -3
View File
@@ -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,
+4 -14
View File
@@ -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,
+16 -62
View File
@@ -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>
+29 -62
View File
@@ -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>
))}
+15 -64
View File
@@ -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>
);
+40 -40
View File
@@ -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>
);
}
-223
View File
@@ -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}
/>
);
}
-618
View File
@@ -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>
);
}
)
);
-219
View File
@@ -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 -4
View File
@@ -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>
+33 -4
View File
@@ -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
*/
-7
View File
@@ -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');
}
+8 -28
View File
@@ -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}
/>
+7 -2
View File
@@ -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 />
);
}
-390
View File
@@ -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]} />;
}
-408
View File
@@ -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>
);
}
)
);
-20
View File
@@ -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
);
}
+33 -35
View File
@@ -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);
+15 -19
View File
@@ -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
View File
@@ -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;
}
}
+9 -7
View File
@@ -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
+13 -11
View File
@@ -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;
});
}
+13 -4
View File
@@ -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) {
+12 -6
View File
@@ -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,
},
};
-2
View File
@@ -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}
+4 -62
View File
@@ -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}
+2 -14
View File
@@ -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;
+2 -2
View File
@@ -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}
-67
View File
@@ -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}
/>
);
})
);
+1 -4
View File
@@ -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]
);
-6
View File
@@ -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}
-11
View File
@@ -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 -24
View File
@@ -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}
+1 -17
View File
@@ -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}
+1 -9
View File
@@ -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}
-2
View File
@@ -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}
-27
View File
@@ -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);
});
+17 -3
View File
@@ -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]'
-30
View File
@@ -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",