diff --git a/.changeset/add-gif-slash-command.md b/.changeset/add-gif-slash-command.md new file mode 100644 index 000000000..472c7f028 --- /dev/null +++ b/.changeset/add-gif-slash-command.md @@ -0,0 +1,5 @@ +--- +sable: minor +--- + +Add a `/gif` composer command that opens the GIF picker. diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 6c8d78a04..a9f6864fa 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -455,6 +455,7 @@ type EmojiBoardProps = { onCustomEmojiSelect?: (mxc: string, shortcode: string) => void; onStickerSelect?: (mxc: string, shortcode: string, label: string) => void; onGifSelect?: (gif: GifData, spoiler?: boolean) => void; + initialGifSearch?: string; allowTextCustomEmoji?: boolean; addToRecentEmoji?: boolean; isFullWidth?: boolean; @@ -474,6 +475,7 @@ export function EmojiBoard({ onCustomEmojiSelect, onStickerSelect, onGifSelect, + initialGifSearch, allowTextCustomEmoji, addToRecentEmoji = true, isFullWidth, @@ -576,6 +578,13 @@ export function EmojiBoard({ { wait: 200 } ); + useEffect(() => { + if (!gifTab || initialGifSearch === undefined) return; + setShowFavoritesOnly(initialGifSearch.length === 0); + if (initialGifSearch) searchGifs(initialGifSearch); + else resetGifSearch(); + }, [gifTab, initialGifSearch, searchGifs, resetGifSearch]); + const contentScrollRef = useRef(null); const virtualBaseRef = useRef(null); const virtualizer = useVirtualizer({ @@ -677,10 +686,11 @@ export function EmojiBoard({ {onTabChange && } diff --git a/src/app/components/emoji-board/components/SearchInput.tsx b/src/app/components/emoji-board/components/SearchInput.tsx index cabcab0fa..bcf072dc7 100644 --- a/src/app/components/emoji-board/components/SearchInput.tsx +++ b/src/app/components/emoji-board/components/SearchInput.tsx @@ -7,6 +7,7 @@ import { EmojiBoardTab } from '../types'; type SearchInputProps = { query?: string; + defaultValue?: string; onChange: ChangeEventHandler; allowTextCustomEmoji?: boolean; onTextCustomEmojiSelect?: (text: string) => void; @@ -14,6 +15,7 @@ type SearchInputProps = { }; export function SearchInput({ query, + defaultValue, onChange, allowTextCustomEmoji, onTextCustomEmojiSelect, @@ -40,6 +42,7 @@ export function SearchInput({ : 'Search' } maxLength={50} + defaultValue={defaultValue} after={ allowTextCustomEmoji && query && tab !== EmojiBoardTab.Gif ? ( void; +const GIF_COMMAND = 'gif'; type CommandAutocompleteProps = { room: Room; @@ -41,7 +42,10 @@ export function CommandAutocomplete({ }: CommandAutocompleteProps) { const mx = useMatrixClient(); const commands = useCommands(mx, room); - const commandNames = useMemo(() => Object.keys(commands) as Command[], [commands]); + const commandNames = useMemo( + () => [GIF_COMMAND, ...(Object.keys(commands) as Command[])], + [commands] + ); const [result, search, resetSearch] = useAsyncSearch( commandNames, @@ -106,7 +110,9 @@ export function CommandAutocomplete({ {`/${commandName}`} - {commands[commandName].description} + {commandName === GIF_COMMAND + ? 'Search and send a GIF: /gif ' + : commands[commandName as Command].description} diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index d081c7e22..26fc9ba85 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -521,11 +521,13 @@ export const RoomInput = forwardRef( const [sendError, setSendError] = useState(); const isEncrypted = room.hasEncryptionStateEvent(); const [emojiBoardTab, setEmojiBoardTab] = useState(undefined); + const [initialGifSearch, setInitialGifSearch] = useState(); const closeEmojiBoard = useCallback(() => { if (isMobileOrTablet()) { const activeElement = document.activeElement; if (activeElement instanceof HTMLElement) activeElement.blur(); } + setInitialGifSearch(undefined); setEmojiBoardTab(undefined); }, []); const toggleEmojiBoardTab = useCallback((tab: EmojiBoardTab) => { @@ -1212,6 +1214,8 @@ export const RoomInput = forwardRef( room, }) ); + const rawGifCommand = + commandName === undefined ? plainText.match(/^\/gif(?:\s+(.*))?$/i) : undefined; let msgType = MsgType.Text; @@ -1228,6 +1232,15 @@ export const RoomInput = forwardRef( return; // don't do anything besides handling the command } + if (rawGifCommand) { + setInitialGifSearch(rawGifCommand[1] ?? ''); + setEmojiBoardTab(EmojiBoardTab.Gif); + resetEditor(editor); + resetEditorHistory(editor); + sendTypingStatus(false); + return; + } + if (commandName) { plainText = trimCommand(commandName, plainText); customHtml = trimCommand(commandName, customHtml); @@ -1249,7 +1262,10 @@ export const RoomInput = forwardRef( if ((commandName as Command) === Command.Poll) setShowPollPicker(true); else if ((commandName as Command) === Command.Location && plainText.trim().length === 0) setShowLocationPicker(true); - else { + else if (commandName === 'gif') { + setInitialGifSearch(plainText); + setEmojiBoardTab(EmojiBoardTab.Gif); + } else { const commandContent = commands[commandName as Command]; if (commandContent) { commandContent.exe(plainText, customHtml); @@ -2221,6 +2237,7 @@ export const RoomInput = forwardRef( onCustomEmojiSelect={handleEmoticonSelect} onStickerSelect={handleStickerSelect} onGifSelect={handleGifSelect} + initialGifSearch={initialGifSearch} requestClose={closeEmojiBoard} /> );