/* Copyright 2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ import { Button as CpdButton, Tooltip, Search, Form, Alert, } from "@vector-im/compound-web"; import { SearchIcon, CloseIcon, RaisedHandSolidIcon, ReactionIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { ChangeEventHandler, ComponentPropsWithoutRef, FC, KeyboardEventHandler, ReactNode, useCallback, useEffect, useMemo, useState, } from "react"; import { useTranslation } from "react-i18next"; import { logger } from "matrix-js-sdk/src/logger"; import classNames from "classnames"; import { useReactions } from "../useReactions"; import styles from "./ReactionToggleButton.module.css"; import { ReactionOption, ReactionSet, ReactionsRowSize } from "../reactions"; import { Modal } from "../Modal"; interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> { raised: boolean; open: boolean; } const InnerButton: FC = ({ raised, open, ...props }) => { const { t } = useTranslation(); return ( ); }; export function ReactionPopupMenu({ sendReaction, toggleRaisedHand, isHandRaised, canReact, errorText, }: { sendReaction: (reaction: ReactionOption) => void; toggleRaisedHand: () => void; errorText?: string; isHandRaised: boolean; canReact: boolean; }): ReactNode { const { t } = useTranslation(); const [searchText, setSearchText] = useState(""); const [isSearching, setIsSearching] = useState(false); const onSearch = useCallback>((ev) => { ev.preventDefault(); setSearchText(ev.target.value.trim().toLocaleLowerCase()); }, []); const filteredReactionSet = useMemo( () => ReactionSet.filter( (reaction) => !isSearching || (!!searchText && (reaction.name.startsWith(searchText) || reaction.alias?.some((a) => a.startsWith(searchText)))), ).slice(0, ReactionsRowSize), [searchText, isSearching], ); const onSearchKeyDown = useCallback>( (ev) => { if (ev.key === "Enter") { ev.preventDefault(); if (!canReact) { return; } if (filteredReactionSet.length !== 1) { return; } sendReaction(filteredReactionSet[0]); setIsSearching(false); } else if (ev.key === "Escape") { ev.preventDefault(); setIsSearching(false); } }, [sendReaction, filteredReactionSet, canReact, setIsSearching], ); const label = isHandRaised ? t("action.lower_hand", { keyboardShortcut: "H" }) : t("action.raise_hand", { keyboardShortcut: "H" }); return ( <> {errorText && ( {errorText} )}
toggleRaisedHand()} iconOnly Icon={RaisedHandSolidIcon} />
{isSearching ? ( <> setIsSearching(false)} /> ) : null} {filteredReactionSet.map((reaction, index) => (
  • {/* Show the keyboard key assigned to the reaction */} sendReaction(reaction)} aria-keyshortcuts={ index < ReactionsRowSize ? (index + 1).toString() : undefined } > {reaction.emoji}
  • ))}
    {!isSearching ? (
  • setIsSearching(true)} />
  • ) : null}
    ); } interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> { userId: string; } export function ReactionToggleButton({ userId, ...props }: ReactionToggleButtonProps): ReactNode { const { t } = useTranslation(); const { raisedHands, toggleRaisedHand, sendReaction, reactions } = useReactions(); const [busy, setBusy] = useState(false); const [showReactionsMenu, setShowReactionsMenu] = useState(false); const [errorText, setErrorText] = useState(); const isHandRaised = !!raisedHands[userId]; const canReact = !reactions[userId]; useEffect(() => { // Clear whenever the reactions menu state changes. setErrorText(undefined); }, [showReactionsMenu]); const sendRelation = useCallback( async (reaction: ReactionOption) => { try { setBusy(true); await sendReaction(reaction); setErrorText(undefined); setShowReactionsMenu(false); } catch (ex) { setErrorText(ex instanceof Error ? ex.message : "Unknown error"); logger.error("Failed to send reaction", ex); } finally { setBusy(false); } }, [sendReaction], ); const wrappedToggleRaisedHand = useCallback(() => { const toggleHand = async (): Promise => { try { setBusy(true); await toggleRaisedHand(); setShowReactionsMenu(false); } catch (ex) { setErrorText(ex instanceof Error ? ex.message : "Unknown error"); logger.error("Failed to raise/lower hand", ex); } finally { setBusy(false); } }; void toggleHand(); }, [toggleRaisedHand]); return ( <> setShowReactionsMenu((show) => !show)} raised={isHandRaised} open={showReactionsMenu} {...props} /> setShowReactionsMenu(false)} > void sendRelation(reaction)} toggleRaisedHand={wrappedToggleRaisedHand} /> ); }