// Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; import lodash from 'lodash'; import classNames from 'classnames'; import { ContactName } from './ContactName.dom.js'; import type { Props as AvatarProps } from '../Avatar.dom.js'; import { Avatar } from '../Avatar.dom.js'; import { useRestoreFocus } from '../../hooks/useRestoreFocus.dom.js'; import type { ConversationType } from '../../state/ducks/conversations.preload.js'; import type { PreferredBadgeSelectorType } from '../../state/selectors/badges.preload.js'; import { useEscapeHandling } from '../../hooks/useEscapeHandling.dom.js'; import type { ThemeType } from '../../types/Util.std.js'; import type { EmojiParentKey, EmojiVariantKey, } from '../fun/data/emojis.std.js'; import { EMOJI_PARENT_KEY_CONSTANTS, getEmojiParentKeyByVariantKey, getEmojiVariantByKey, getEmojiVariantKeyByValue, isEmojiVariantValue, } from '../fun/data/emojis.std.js'; import { strictAssert } from '../../util/assert.std.js'; import { FunStaticEmoji } from '../fun/FunEmoji.dom.js'; import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.js'; const { mapValues, orderBy } = lodash; export type Reaction = { emoji: string; timestamp: number; from: Pick< ConversationType, | 'acceptedMessageRequest' | 'avatarUrl' | 'badges' | 'color' | 'id' | 'isMe' | 'phoneNumber' | 'profileName' | 'sharedGroupNames' | 'title' >; }; export type OwnProps = { getPreferredBadge: PreferredBadgeSelectorType; reactions: Array; pickedReaction?: string; onClose?: () => unknown; theme: ThemeType; }; export type Props = OwnProps & Pick, 'style'> & Pick; const DEFAULT_EMOJI_ORDER = [ 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 = { count: number; emoji?: string; id: string; index: number; }; type ReactionWithEmojiData = Reaction & Readonly<{ parentKey: EmojiParentKey; variantKey: EmojiVariantKey; }>; function ReactionViewerEmoji(props: { emojiVariantValue: string | undefined; }): JSX.Element { const emojiLocalizer = useFunEmojiLocalizer(); strictAssert(props.emojiVariantValue != null, 'Expected an emoji'); strictAssert( isEmojiVariantValue(props.emojiVariantValue), 'Must be valid emoji variant value' ); const emojiVariantKey = getEmojiVariantKeyByValue(props.emojiVariantValue); const emojiVariant = getEmojiVariantByKey(emojiVariantKey); return ( ); } export const ReactionViewer = React.forwardRef( function ReactionViewerInner( { getPreferredBadge, i18n, onClose, pickedReaction, reactions, theme, ...rest }, ref ) { const reactionsWithEmojiData = React.useMemo( () => reactions .map(reaction => { if (!isEmojiVariantValue(reaction.emoji)) { return null; } const variantKey = getEmojiVariantKeyByValue(reaction.emoji); const parentKey = getEmojiParentKeyByVariantKey(variantKey); return { ...reaction, parentKey, variantKey }; }) .filter((data): data is ReactionWithEmojiData => { return data != null; }), [reactions] ); 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 = React.useMemo( () => [ { id: 'all', index: 0, count: reactionsWithEmojiData.length, }, ...Object.entries(groupedAndSortedReactions) .filter(([key]) => key !== 'all') .map(([, groupedReactions]) => { // Find the local user's reaction first, then fall back to most recent const localUserReaction = groupedReactions.find(r => r.from.isMe); const firstReaction = localUserReaction || groupedReactions[0]; return { id: firstReaction.parentKey, index: DEFAULT_EMOJI_ORDER.includes(firstReaction.parentKey) ? DEFAULT_EMOJI_ORDER.indexOf(firstReaction.parentKey) : Infinity, emoji: firstReaction.emoji, count: groupedReactions.length, }; }), ].sort((a, b) => a.index - b.index), [reactionsWithEmojiData, groupedAndSortedReactions] ); const [selectedReactionCategory, setSelectedReactionCategory] = React.useState(pickedReaction || 'all'); // Handle escape key useEscapeHandling(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('icu:you') ) : ( )}
))}
); } );