// Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; import { groupBy, mapValues, orderBy } from 'lodash'; import classNames from 'classnames'; import { ContactName } from './ContactName'; import { Avatar, Props as AvatarProps } from '../Avatar'; import { Emoji } from '../emoji/Emoji'; import { useRestoreFocus } from '../../util/hooks/useRestoreFocus'; import { ConversationType } from '../../state/ducks/conversations'; import { emojiToData, EmojiData } from '../emoji/lib'; export type Reaction = { emoji: string; timestamp: number; from: Pick< ConversationType, | 'acceptedMessageRequest' | 'avatarPath' | 'color' | 'id' | 'isMe' | 'name' | 'phoneNumber' | 'profileName' | 'sharedGroupNames' | 'title' >; }; export type OwnProps = { reactions: Array; pickedReaction?: string; onClose?: () => unknown; }; export type Props = OwnProps & Pick, 'style'> & Pick; const DEFAULT_EMOJI_ORDER = [ 'heart', '+1', '-1', 'joy', 'open_mouth', 'cry', 'rage', ]; type ReactionCategory = { count: number; emoji?: string; id: string; index: number; }; type ReactionWithEmojiData = Reaction & EmojiData; export const ReactionViewer = React.forwardRef( ({ i18n, reactions, onClose, pickedReaction, ...rest }, ref) => { const reactionsWithEmojiData = React.useMemo( () => reactions .map(reaction => { const emojiData = emojiToData(reaction.emoji); if (!emojiData) { return undefined; } return { ...reaction, ...emojiData, }; }) .filter( ( reactionWithEmojiData ): reactionWithEmojiData is ReactionWithEmojiData => Boolean(reactionWithEmojiData) ), [reactions] ); const groupedAndSortedReactions = React.useMemo( () => mapValues( { all: reactionsWithEmojiData, ...groupBy(reactionsWithEmojiData, 'short_name'), }, groupedReactions => orderBy(groupedReactions, ['timestamp'], ['desc']) ), [reactionsWithEmojiData] ); const reactionCategories: Array = React.useMemo( () => [ { id: 'all', index: 0, count: reactionsWithEmojiData.length, }, ...Object.entries(groupedAndSortedReactions) .filter(([key]) => key !== 'all') .map(([, [{ short_name: id, emoji }, ...otherReactions]]) => { return { id, index: DEFAULT_EMOJI_ORDER.includes(id) ? DEFAULT_EMOJI_ORDER.indexOf(id) : Infinity, emoji, count: otherReactions.length + 1, }; }), ].sort((a, b) => a.index - b.index), [reactionsWithEmojiData, groupedAndSortedReactions] ); const [ selectedReactionCategory, setSelectedReactionCategory, ] = React.useState(pickedReaction || 'all'); // Handle escape key React.useEffect(() => { const handler = (e: KeyboardEvent) => { if (onClose && e.key === 'Escape') { onClose(); } }; document.addEventListener('keydown', handler); return () => { document.removeEventListener('keydown', handler); }; }, [onClose]); // Focus first button and restore focus on unmount const [focusRef] = useRestoreFocus(); // If we have previously selected a reaction type that is no longer present // (removed on another device, for instance) we should select another // reaction type React.useEffect(() => { if ( !reactionCategories.find(({ id }) => id === selectedReactionCategory) ) { if (reactionsWithEmojiData.length > 0) { setSelectedReactionCategory('all'); } else if (onClose) { onClose(); } } }, [ reactionCategories, onClose, reactionsWithEmojiData, selectedReactionCategory, ]); const selectedReactions = groupedAndSortedReactions[selectedReactionCategory] || []; return (
{reactionCategories.map(({ id, emoji, count }, index) => { const isAll = index === 0; const maybeFocusRef = isAll ? focusRef : undefined; return ( ); })}
{selectedReactions.map(({ from, emoji }) => (
{from.isMe ? ( i18n('you') ) : ( )}
))}
); } );