import * as React from 'react'; import classNames from 'classnames'; import { noop } from 'lodash'; import { Manager, Popper, Reference } from 'react-popper'; import { createPortal } from 'react-dom'; import { StickerPicker } from './StickerPicker'; import { countStickers } from './lib'; import { StickerPackType, StickerType } from '../../state/ducks/stickers'; import { LocalizerType } from '../../types/Util'; export type OwnProps = { readonly i18n: LocalizerType; readonly receivedPacks: ReadonlyArray; readonly installedPacks: ReadonlyArray; readonly blessedPacks: ReadonlyArray; readonly knownPacks: ReadonlyArray; readonly installedPack?: StickerPackType | null; readonly recentStickers: ReadonlyArray; readonly clearInstalledStickerPack: () => unknown; readonly onClickAddPack: () => unknown; readonly onPickSticker: (packId: string, stickerId: number) => unknown; readonly showIntroduction?: boolean; readonly clearShowIntroduction: () => unknown; readonly showPickerHint: boolean; readonly clearShowPickerHint: () => unknown; readonly position?: 'top-end' | 'top-start'; }; export type Props = OwnProps; export const StickerButton = React.memo( // tslint:disable-next-line max-func-body-length ({ i18n, clearInstalledStickerPack, onClickAddPack, onPickSticker, recentStickers, receivedPacks, installedPack, installedPacks, blessedPacks, knownPacks, showIntroduction, clearShowIntroduction, showPickerHint, clearShowPickerHint, position = 'top-end', }: Props) => { const [open, setOpen] = React.useState(false); const [popperRoot, setPopperRoot] = React.useState( null ); 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, onClickAddPack, installedPacks, popperRoot, setOpen, ] ); const handlePickSticker = React.useCallback( (packId: string, stickerId: number) => { setOpen(false); onPickSticker(packId, stickerId); }, [setOpen, onPickSticker] ); const handleClose = React.useCallback( () => { setOpen(false); }, [setOpen] ); const handleClickAddPack = React.useCallback( () => { setOpen(false); if (showPickerHint) { clearShowPickerHint(); } onClickAddPack(); }, [onClickAddPack, showPickerHint, 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); const handleOutsideClick = ({ target }: MouseEvent) => { const targetElement = target as HTMLElement; const className = 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 = !className || className.indexOf('module-sticker-picker__header__button') < 0; if (!root.contains(targetElement) && isMissingButtonClass) { setOpen(false); } }; document.addEventListener('click', handleOutsideClick); return () => { document.body.removeChild(root); document.removeEventListener('click', handleOutsideClick); setPopperRoot(null); }; } return noop; }, [open, setOpen, setPopperRoot] ); // Install keyboard shortcut to open sticker picker React.useEffect( () => { const handleKeydown = (event: KeyboardEvent) => { const { ctrlKey, key, metaKey, shiftKey } = event; const ctrlOrCommand = metaKey || ctrlKey; if (ctrlOrCommand && shiftKey && (key === 's' || key === 'S')) { 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) { // tslint:disable-next-line:no-string-based-set-timeout const timerId = setTimeout(clearInstalledStickerPack, 60 * 1000); return () => { clearTimeout(timerId); }; } return noop; }, [installedPack, clearInstalledStickerPack] ); if ( countStickers({ knownPacks, blessedPacks, installedPacks, receivedPacks, }) === 0 ) { return null; } return ( {({ ref }) => ( )} ) : null} {!open && showIntroduction ? ( {({ ref, style, placement, arrowProps }) => ( )} ) : null} {open && popperRoot ? createPortal( {({ ref, style }) => ( )} , popperRoot ) : null} ); } );