diff --git a/.changeset/cuddly-garlics-cover.md b/.changeset/cuddly-garlics-cover.md new file mode 100644 index 0000000000000..dcedaa49575d7 --- /dev/null +++ b/.changeset/cuddly-garlics-cover.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes unused `i18nTitle` provided by the app in message composer popup previewer diff --git a/apps/meteor/client/views/room/composer/ComposerBoxPopupPreview.tsx b/apps/meteor/client/views/room/composer/ComposerBoxPopupPreview.tsx index d84eeb0c8e648..df17ff5d5d8de 100644 --- a/apps/meteor/client/views/room/composer/ComposerBoxPopupPreview.tsx +++ b/apps/meteor/client/views/room/composer/ComposerBoxPopupPreview.tsx @@ -1,6 +1,6 @@ import { Box, Skeleton, Tile, Option } from '@rocket.chat/fuselage'; import { useMethod } from '@rocket.chat/ui-contexts'; -import type { ForwardedRef } from 'react'; +import type { ForwardedRef, ReactNode } from 'react'; import { forwardRef, useEffect, useId, useImperativeHandle } from 'react'; import type { ComposerBoxPopupProps } from './ComposerBoxPopup'; @@ -9,13 +9,14 @@ import { useChat } from '../contexts/ChatContext'; type ComposerBoxPopupPreviewItem = { _id: string; type: 'image' | 'video' | 'audio' | 'text' | 'other'; value: string; sort?: number }; type ComposerBoxPopupPreviewProps = ComposerBoxPopupProps & { + title?: ReactNode; rid: string; tmid?: string; - suspended?: boolean; + suspended: boolean; }; const ComposerBoxPopupPreview = forwardRef(function ComposerBoxPopupPreview( - { focused, items, rid, tmid, select, suspended }: ComposerBoxPopupPreviewProps, + { focused, items, title, rid, tmid, select, suspended }: ComposerBoxPopupPreviewProps, ref: ForwardedRef< | { getFilter?: () => unknown; @@ -27,6 +28,7 @@ const ComposerBoxPopupPreview = forwardRef(function ComposerBoxPopupPreview( const id = useId(); const chat = useChat(); const executeSlashCommandPreviewMethod = useMethod('executeSlashCommandPreview'); + useImperativeHandle( ref, () => ({ @@ -96,48 +98,55 @@ const ComposerBoxPopupPreview = forwardRef(function ComposerBoxPopupPreview( return ( - - - {isLoading && - Array(5) - .fill(5) - .map((_, index) => )} - - {!isLoading && - itemsFlat.map((item) => ( - select(item)} - role='option' - className={['popup-item', item === focused && 'selected'].filter(Boolean).join(' ')} - id={`popup-item-${item._id}`} - key={item._id} - bg={item === focused ? 'selected' : undefined} - borderColor={item === focused ? 'highlight' : 'transparent'} - tabIndex={item === focused ? 0 : -1} - aria-selected={item === focused} - m={2} - borderWidth='default' - borderRadius='x4' - > - {item.type === 'image' && {item._id}} - {item.type === 'audio' && ( - - )} - {item.type === 'video' && ( - - )} - {item.type === 'text' && } - {item.type === 'other' && {item.value}} - - ))} + + {title && ( + + {title} + + )} + + + {isLoading && + Array(5) + .fill(5) + .map((_, index) => )} + + {!isLoading && + itemsFlat.map((item) => ( + select(item)} + role='option' + className={['popup-item', item === focused && 'selected'].filter(Boolean).join(' ')} + id={`popup-item-${item._id}`} + key={item._id} + bg={item === focused ? 'selected' : undefined} + borderColor={item === focused ? 'highlight' : 'transparent'} + tabIndex={item === focused ? 0 : -1} + aria-selected={item === focused} + m={2} + borderWidth='default' + borderRadius='x4' + > + {item.type === 'image' && {item._id}} + {item.type === 'audio' && ( + + )} + {item.type === 'video' && ( + + )} + {item.type === 'text' && } + {item.type === 'other' && {item.value}} + + ))} + diff --git a/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopup.ts b/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopup.ts index 1c08b3118fc11..6ec1502dda217 100644 --- a/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopup.ts +++ b/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopup.ts @@ -9,7 +9,7 @@ import type { ComposerPopupOption } from '../../contexts/ComposerPopupContext'; type ComposerBoxPopupImperativeCommands = MutableRefObject< | { - getFilter?: () => unknown; + getFilter?: () => string; select?: (s: T) => void; } | undefined @@ -19,66 +19,60 @@ type ComposerBoxPopupOptions = | { - popup: ComposerPopupOption; + option: ComposerPopupOption; items: UseQueryResult[]; focused: T | undefined; - ariaActiveDescendant: string | undefined; select: (item: T) => void; callbackRef: (node: HTMLElement) => void; commandsRef: ComposerBoxPopupImperativeCommands; suspended: boolean; filter: unknown; - clearPopup: () => void; + clear: () => void; } | { - popup: undefined; + option: undefined; items: undefined; focused: undefined; - ariaActiveDescendant: undefined; callbackRef: (node: HTMLElement) => void; select: undefined; commandsRef: ComposerBoxPopupImperativeCommands; - suspended: boolean; + suspended: undefined; filter: unknown; - clearPopup: () => void; + clear: () => void; }; const keys = { TAB: 9, ENTER: 13, ESC: 27, - ARROW_LEFT: 37, ARROW_UP: 38, - ARROW_RIGHT: 39, ARROW_DOWN: 40, -}; +} as const; -export const useComposerBoxPopup = ({ - configurations, -}: { - configurations: ComposerBoxPopupOptions[]; -}): ComposerBoxPopupResult => { - const [popup, setPopup] = useState | undefined>(undefined); +export const useComposerBoxPopup = ( + options: ComposerBoxPopupOptions[], +): ComposerBoxPopupResult => { + const [optionIndex, setOptionIndex] = useState(-1); const [focused, setFocused] = useState(undefined); - const [filter, setFilter] = useState(''); + const [filter, setFilter] = useState(''); + + const option = options[optionIndex]; const commandsRef: ComposerBoxPopupImperativeCommands = useRef(); - const { queries: items, suspended } = useComposerBoxPopupQueries(filter, popup) as { + const { queries: items, suspended } = useComposerBoxPopupQueries(filter, option) as { queries: UseQueryResult[]; suspended: boolean; }; const chat = useChat(); - const ariaActiveDescendant = focused ? `popup-item-${focused._id}` : undefined; - useEffect(() => { - if (!popup) { + if (!option) { return; } - if (popup?.preview && suspended) { + if (option?.preview && suspended) { setFocused(undefined); return; } @@ -89,10 +83,10 @@ export const useComposerBoxPopup = ({ .sort((a, b) => (('sort' in a && a.sort) || 0) - (('sort' in b && b.sort) || 0)); return sortedItems.find((item) => item._id === focused?._id) ?? sortedItems[0]; }); - }, [items, popup, suspended]); + }, [items, option, suspended]); const select = useEffectEvent((item: T) => { - if (!popup) { + if (!option) { throw new Error('No popup is open'); } @@ -101,33 +95,33 @@ export const useComposerBoxPopup = ({ } else { const value = chat?.composer?.substring(0, chat?.composer?.selection.start); const selector = - popup.matchSelectorRegex ?? - (popup.triggerAnywhere ? new RegExp(`(?:^| |\n)(${popup.trigger})([^\\s]*$)`) : new RegExp(`(?:^)(${popup.trigger})([^\\s]*$)`)); + option.matchSelectorRegex ?? + (option.triggerAnywhere ? new RegExp(`(?:^| |\n)(${option.trigger})([^\\s]*$)`) : new RegExp(`(?:^)(${option.trigger})([^\\s]*$)`)); const result = value?.match(selector); if (!result || !value) { return; } - chat?.composer?.replaceText((popup.prefix ?? popup.trigger ?? '') + popup.getValue(item) + (popup.suffix ?? ''), { + chat?.composer?.replaceText((option.prefix ?? option.trigger ?? '') + option.getValue(item) + (option.suffix ?? ''), { start: value.lastIndexOf(result[1] + result[2]), end: chat?.composer?.selection.start, }); } - setPopup(undefined); + setOptionIndex(-1); setFocused(undefined); }); - const setConfigByInput = useEffectEvent((): ComposerBoxPopupOptions | undefined => { + const setOptionByInput = useEffectEvent((): ComposerBoxPopupOptions | undefined => { const value = chat?.composer?.substring(0, chat?.composer?.selection.start); if (!value) { - setPopup(undefined); + setOptionIndex(-1); setFocused(undefined); return; } - const configuration = configurations.find(({ trigger, matchSelectorRegex, triggerAnywhere, triggerLength }) => { + const optionIndex = options.findIndex(({ trigger, matchSelectorRegex, triggerAnywhere, triggerLength }) => { const selector = matchSelectorRegex ?? (triggerAnywhere ? new RegExp(`(?:^| |\n)(${trigger})[^\\s]*$`) : new RegExp(`(?:^)(${trigger})[^\\s]*$`)); const result = selector.test(value); @@ -137,50 +131,49 @@ export const useComposerBoxPopup = ({ const filter = value.match(selector); return filter && triggerLength < filter[0].length; }); - setPopup(configuration); - if (!configuration) { + setOptionIndex(optionIndex); + const option = options[optionIndex]; + if (!option) { setFocused(undefined); setFilter(''); } - if (configuration) { + if (option) { const selector = - configuration.matchSelectorRegex ?? - (configuration.triggerAnywhere - ? new RegExp(`(?:^| |\n)(${configuration.trigger})([^\\s]*$)`) - : new RegExp(`(?:^)(${configuration.trigger})([^\\s]*$)`)); + option.matchSelectorRegex ?? + (option.triggerAnywhere ? new RegExp(`(?:^| |\n)(${option.trigger})([^\\s]*$)`) : new RegExp(`(?:^)(${option.trigger})([^\\s]*$)`)); const result = value.match(selector); setFilter(commandsRef.current?.getFilter?.() ?? (result ? result[2] : '')); } - return configuration; + return option; }); - const onFocus = useEffectEvent(() => { - if (popup) { + const handleFocus = useEffectEvent(() => { + if (option) { return; } - setConfigByInput(); + setOptionByInput(); }); - const keyup = useEffectEvent((event: KeyboardEvent) => { - if (!setConfigByInput()) { + const handleKeyUp = useEffectEvent((event: KeyboardEvent) => { + if (!setOptionByInput()) { return; } - if (!popup) { + if (!option) { return; } - if (popup.closeOnEsc === true && event.which === keys.ESC) { - setPopup(undefined); + if (option.closeOnEsc === true && event.which === keys.ESC) { + setOptionIndex(-1); setFocused(undefined); event.preventDefault(); event.stopImmediatePropagation(); } }); - const keydown = useEffectEvent((event: KeyboardEvent) => { - if (!popup) { + const handleKeyDown = useEffectEvent((event: KeyboardEvent) => { + if (!option) { return; } @@ -235,54 +228,59 @@ export const useComposerBoxPopup = ({ } }); - const clearPopup = useEffectEvent(() => { - if (!popup) { + const clear = useEffectEvent(() => { + if (!option) { return; } - setPopup(undefined); + setOptionIndex(-1); setFocused(undefined); setFilter(''); }); + const ref = useRef(null); const callbackRef = useCallback( (node: HTMLElement | null) => { - if (!node) { - return; + if (ref.current) { + ref.current.removeEventListener('keyup', handleKeyUp); + ref.current.removeEventListener('keydown', handleKeyDown); + ref.current.removeEventListener('focus', handleFocus); + ref.current = null; } - node.addEventListener('keyup', keyup); - node.addEventListener('keydown', keydown); - node.addEventListener('focus', onFocus); + if (node) { + ref.current = node; + node.addEventListener('keyup', handleKeyUp); + node.addEventListener('keydown', handleKeyDown); + node.addEventListener('focus', handleFocus); + } }, - [keyup, keydown, onFocus], + [handleKeyUp, handleKeyDown, handleFocus], ); - if (!popup) { + if (!option) { return { - callbackRef, - focused: undefined, + option: undefined, items: undefined, - ariaActiveDescendant: undefined, - popup: undefined, + focused: undefined, select: undefined, - suspended: true, + callbackRef, commandsRef, + suspended: undefined, filter: undefined, - clearPopup, + clear, }; } return { - focused, + option, items, - ariaActiveDescendant, - popup, + focused, select, - filter, - suspended, - commandsRef, callbackRef, - clearPopup, + commandsRef, + suspended, + filter, + clear, }; }; diff --git a/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts b/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts index 4247661eca499..02f129df3b6db 100644 --- a/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts +++ b/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts @@ -11,9 +11,9 @@ const isMutableRefObject = (x: unknown): x is MutableRefObject => typeof x * @param refs The refs to merge. * @returns The merged ref callback. */ -export const useMessageComposerMergedRefs = (...refs: Ref[]): RefCallback => { +export const useMessageComposerMergedRefs = (...refs: (Ref | undefined)[]): RefCallback => { return useCallback((refValue: T) => { - refs.filter(Boolean).forEach((ref) => { + refs.forEach((ref) => { if (isRefCallback(ref)) { ref(refValue); return; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 3fd73b6a000db..05f93f5c73ce1 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -32,7 +32,7 @@ import { keyCodes } from '../../../../lib/utils/keyCodes'; import AudioMessageRecorder from '../../../composer/AudioMessageRecorder'; import VideoMessageRecorder from '../../../composer/VideoMessageRecorder'; import { useChat } from '../../contexts/ChatContext'; -import { useComposerPopup } from '../../contexts/ComposerPopupContext'; +import { useComposerPopupOptions } from '../../contexts/ComposerPopupContext'; import { useRoom } from '../../contexts/RoomContext'; import ComposerBoxPopup from '../ComposerBoxPopup'; import ComposerBoxPopupPreview from '../ComposerBoxPopupPreview'; @@ -161,7 +161,7 @@ const MessageBox = ({ const handleSendMessage = useEffectEvent(() => { const text = chat.composer?.text ?? ''; chat.composer?.clear(); - clearPopup(); + popup.clear(); onSend?.({ value: text, @@ -327,22 +327,8 @@ const MessageBox = ({ } }); - const composerPopupConfig = useComposerPopup(); - - const { - popup, - focused, - items, - ariaActiveDescendant, - suspended, - select, - commandsRef, - callbackRef: c, - filter, - clearPopup, - } = useComposerBoxPopup<{ _id: string; sort?: number }>({ - configurations: composerPopupConfig, - }); + const popupOptions = useComposerPopupOptions(); + const popup = useComposerBoxPopup(popupOptions); const keyDownHandlerCallbackRef = useCallback( (node: HTMLTextAreaElement) => { @@ -356,15 +342,21 @@ const MessageBox = ({ [handler], ); - const mergedRefs = useMessageComposerMergedRefs(c, textareaRef, callbackRef, autofocusRef, keyDownHandlerCallbackRef); + const mergedRefs = useMessageComposerMergedRefs(popup.callbackRef, textareaRef, callbackRef, autofocusRef, keyDownHandlerCallbackRef); - const shouldPopupPreview = useEnablePopupPreview(filter, popup); + const shouldPopupPreview = useEnablePopupPreview(popup.filter, popup.option); return ( <> {chat.composer?.quotedMessages && } - {shouldPopupPreview && popup && ( - + {shouldPopupPreview && popup.option && ( + )} {/* SlashCommand Preview popup works in a weird way @@ -372,16 +364,17 @@ const MessageBox = ({ After that we need to the slashcommand list and check if the command exists and provide the preview if not the query is `suspend` which means the slashcommand is not found or doesn't have a preview */} - {popup?.preview && ( + {popup.option?.preview && ( )}
diff --git a/apps/meteor/client/views/room/contexts/ComposerPopupContext.ts b/apps/meteor/client/views/room/contexts/ComposerPopupContext.ts index e42bbe5e38fef..bf0b534c5ae04 100644 --- a/apps/meteor/client/views/room/contexts/ComposerPopupContext.ts +++ b/apps/meteor/client/views/room/contexts/ComposerPopupContext.ts @@ -42,10 +42,10 @@ export const createMessageBoxPopupConfig = { +export const useComposerPopupOptions = () => { const composerPopupContext = useContext(ComposerPopupContext); if (!composerPopupContext) { - throw new Error('useComposerPopup must be used within ComposerPopupContext'); + throw new Error('useComposerPopupOptions must be used within ComposerPopupContext'); } return composerPopupContext; }; diff --git a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx index 7508dc5bbb985..22557f4a6d15b 100644 --- a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx +++ b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx @@ -3,7 +3,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { useMethod, useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; @@ -36,6 +36,7 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = const suggestionsCount = useSetting('Number_of_users_autocomplete_suggestions', 5); const cannedResponseEnabled = useSetting('Canned_Responses_Enable', true); const [recentEmojis] = useLocalStorage('emoji.recent', []); + const [previewTitle, setPreviewTitle] = useState(''); const isOmnichannel = isOmnichannelRoom(room); const useEmoji = useUserPreference('useEmojis'); const { t, i18n } = useTranslation(); @@ -357,10 +358,14 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = }, }), createMessageBoxPopupConfig({ + title: previewTitle, matchSelectorRegex: /(?:^)(\/[\w\d\S]+ )[^]*$/, preview: true, getItemsFromLocal: async ({ cmd, params, tmid }: { cmd: string; params: string; tmid: string }) => { const result = await call({ cmd, params, msg: { rid, tmid } }); + + setPreviewTitle(t(result?.i18nTitle ?? '')); + return ( result?.items.map((item) => ({ _id: item.id, @@ -371,7 +376,21 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = }, }), ].filter(Boolean); - }, [t, i18n, cannedResponseEnabled, isOmnichannel, recentEmojis, suggestionsCount, userSpotlight, rid, call, useEmoji, encrypted]); + }, [ + t, + useEmoji, + encrypted, + cannedResponseEnabled, + isOmnichannel, + previewTitle, + suggestionsCount, + userSpotlight, + rid, + recentEmojis, + i18n, + call, + setPreviewTitle, + ]); return ; };