// 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 { get, noop } from 'lodash'; import { Manager, Popper, Reference } from 'react-popper'; import { Emoji } from './Emoji'; import type { Props as EmojiPickerProps } from './EmojiPicker'; import { EmojiPicker } from './EmojiPicker'; import type { LocalizerType } from '../../types/Util'; import { useRefMerger } from '../../hooks/useRefMerger'; import { handleOutsideClick } from '../../util/handleOutsideClick'; import * as KeyboardLayout from '../../services/keyboardLayout'; export enum EmojiButtonVariant { Normal, ProfileEditor, } export type OwnProps = Readonly<{ className?: string; closeOnPick?: boolean; emoji?: string; i18n: LocalizerType; onClose?: () => unknown; onOpen?: () => unknown; emojiButtonApi?: MutableRefObject; variant?: EmojiButtonVariant; }>; export type Props = OwnProps & Pick< EmojiPickerProps, 'onPickEmoji' | 'onSetSkinTone' | 'recentEmojis' | 'skinTone' >; export type EmojiButtonAPI = Readonly<{ close: () => void; }>; export const EmojiButton = React.memo(function EmojiButtonInner({ className, closeOnPick, emoji, emojiButtonApi, i18n, onClose, onOpen, onPickEmoji, skinTone, onSetSkinTone, recentEmojis, variant = EmojiButtonVariant.Normal, }: Props) { const isRTL = i18n.getLocaleDirection() === 'rtl'; const [open, setOpen] = React.useState(false); const buttonRef = React.useRef(null); const popperRef = React.useRef(null); const refMerger = useRefMerger(); React.useEffect(() => { if (!open) { return; } onOpen?.(); }, [open, onOpen]); const handleClickButton = React.useCallback(() => { if (open) { setOpen(false); } else { setOpen(true); } }, [open, setOpen]); const handleClose = React.useCallback(() => { setOpen(false); if (onClose) { onClose(); } }, [setOpen, onClose]); const api = React.useMemo( () => ({ close: () => setOpen(false), }), [setOpen] ); 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 conversation has any panels open const panels = document.querySelectorAll('.conversation .panel'); if (panels && panels.length > 1) { return; } if (commandOrCtrl && shiftKey && (key === 'j' || key === 'J')) { event.stopPropagation(); event.preventDefault(); setOpen(!open); } }; document.addEventListener('keydown', handleKeydown); return () => { document.removeEventListener('keydown', handleKeydown); }; }, [open, setOpen]); return ( {({ ref }) => ( )} {open ? (
{({ ref, style }) => ( { onPickEmoji(ev); if (closeOnPick) { handleClose(); } }} onClose={handleClose} skinTone={skinTone} onSetSkinTone={onSetSkinTone} recentEmojis={recentEmojis} /> )}
) : null}
); });