From 7b0dc520792df44ea2f330df40cd7904f206f356 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Sat, 11 Apr 2026 19:16:31 +0100 Subject: [PATCH 01/10] feat: add GIF support --- src/app/components/emoji-board/EmojiBoard.tsx | 218 ++++++++++++++++-- .../emoji-board/components/Group.tsx | 18 +- .../emoji-board/components/Item.tsx | 52 ++++- .../emoji-board/components/Layout.tsx | 8 +- .../emoji-board/components/SearchInput.tsx | 7 +- .../emoji-board/components/Tabs.tsx | 12 + .../emoji-board/components/styles.css.ts | 59 +++++ src/app/components/emoji-board/types.ts | 11 + src/app/features/room/RoomInput.tsx | 75 +++++- 9 files changed, 422 insertions(+), 38 deletions(-) diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 193caf71d..ec5740ae0 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -8,6 +8,7 @@ import { useEffect, useMemo, useRef, + useState, } from 'react'; import { Box, config, Icons, Scroll } from 'folds'; import FocusTrap from 'focus-trap-react'; @@ -46,6 +47,7 @@ import { PreviewData, EmojiItem, StickerItem, + GifItem, CustomEmojiItem, ImageGroupIcon, GroupIcon, @@ -53,7 +55,7 @@ import { EmojiGroup, EmojiBoardLayout, } from './components'; -import { EmojiBoardTab, EmojiType } from './types'; +import { EmojiBoardTab, EmojiType, GifData } from './types'; const RECENT_GROUP_ID = 'recent_group'; const SEARCH_GROUP_ID = 'search_group'; @@ -68,11 +70,17 @@ type StickerGroupItem = { name: string; items: Array; }; +type GifGroupItem = { + id: string; + name: string; + items: GifData[]; +}; const useGroups = ( tab: EmojiBoardTab, - imagePacks: ImagePack[] -): [EmojiGroupItem[], StickerGroupItem[]] => { + imagePacks: ImagePack[], + gifs: GifData[] +): [EmojiGroupItem[], StickerGroupItem[], GifGroupItem[]] => { const mx = useMatrixClient(); const recentEmojis = useRecentEmoji(mx, 21); @@ -132,17 +140,60 @@ const useGroups = ( return g; }, [mx, imagePacks, tab]); - return [emojiGroupItems, stickerGroupItems]; + // TODO: verify this implementation + const gifGroupItems = useMemo(() => { + if (tab !== EmojiBoardTab.Gif) return []; + return [ + { + id: 'gif_group', + name: 'GIFs', + items: gifs, + }, + ]; + }, [tab, gifs]); + + return [emojiGroupItems, stickerGroupItems, gifGroupItems]; }; const useItemRenderer = (tab: EmojiBoardTab, saveStickerEmojiBandwidth: boolean) => { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); - const renderItem = (emoji: IEmoji | PackImageReader, index: number) => { - if ('unicode' in emoji) { - return ; + const renderItem = (item: IEmoji | PackImageReader | GifData, index: number) => { + if (tab === EmojiBoardTab.Gif) { + const gif = item as GifData; + const aspectRatio = + gif.width && gif.height && gif.width > 0 && gif.height > 0 + ? `${gif.width} / ${gif.height}` + : '1 / 1'; + + return ( + + + + ); + } + + if ('unicode' in item) { + return ; } + + const emoji = item as PackImageReader; + if (tab === EmojiBoardTab.Sticker) { return ( void; onCustomEmojiSelect?: (mxc: string, shortcode: string) => void; onStickerSelect?: (mxc: string, shortcode: string, label: string) => void; + onGifSelect?: (gif: GifData) => void; allowTextCustomEmoji?: boolean; addToRecentEmoji?: boolean; }; @@ -395,6 +449,7 @@ export function EmojiBoard({ onEmojiSelect, onCustomEmojiSelect, onStickerSelect, + onGifSelect, allowTextCustomEmoji, addToRecentEmoji = true, }: Readonly) { @@ -402,6 +457,7 @@ export function EmojiBoard({ const [saveStickerEmojiBandwidth] = useSetting(settingsAtom, 'saveStickerEmojiBandwidth'); const emojiTab = tab === EmojiBoardTab.Emoji; + const gifTab = tab === EmojiBoardTab.Gif; const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker; const previewAtom = useMemo( @@ -411,9 +467,6 @@ export function EmojiBoard({ const activeGroupIdAtom = useMemo(() => atom(undefined), []); const setActiveGroupId = useSetAtom(activeGroupIdAtom); const imagePacks = useRelevantImagePacks(usage, imagePackRooms); - const [emojiGroupItems, stickerGroupItems] = useGroups(tab, imagePacks); - const groups = emojiTab ? emojiGroupItems : stickerGroupItems; - const renderItem = useItemRenderer(tab, saveStickerEmojiBandwidth); const searchList = useMemo(() => { let list: Array = []; @@ -430,14 +483,121 @@ export function EmojiBoard({ const searchedItems = result?.items.slice(0, 100); + function useGifSearch() { + const [gifs, setGifs] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const parseTenorResult = useCallback((tenorResult: any): GifData => { + const SIZE_LIMIT = 3 * 1024 * 1024; // 3MB + + const formats = tenorResult.media_formats || {}; + const preview = formats.tinygif || formats.nanogif || formats.mediumgif; + + // Start with full resolution GIF + let fullRes = formats.gif; + // If full res is too large and medium exists, use medium instead + if (fullRes && fullRes.size > SIZE_LIMIT && formats.mediumgif) { + fullRes = formats.mediumgif; + } + + // Fallback if no suitable format found + if (!fullRes) { + fullRes = formats.mediumgif || formats.gif || preview; + } + + // Get dimensions from the selected full resolution format + const dimensions = fullRes?.dims || preview?.dims || [0, 0]; + + // Convert URLs to use proxy + const convertUrl = (url: string): string => { + if (!url) return ''; + try { + const originalUrl = new URL(url); + // TODO: FIX API URL, must be changed when we migrate it to KLIPY + const proxyUrl = new URL('https://proxy.commet.chat'); + proxyUrl.pathname = `/proxy/tenor/media${originalUrl.pathname}`; + return proxyUrl.toString(); + } catch { + // Return original URL as fallback + return url; + } + }; + + return { + id: tenorResult.id, + title: tenorResult.content_description || tenorResult.h1_title || 'GIF', + url: convertUrl(fullRes?.url || ''), + preview_url: convertUrl(preview?.url || fullRes?.url || ''), + width: dimensions[0] || 0, + height: dimensions[1] || 0, + }; + }, []); + + const searchGifs = useCallback( + async (query: string) => { + const trimmedQuery = query.trim(); + + setLoading(true); + setError(null); + + try { + // TODO: FIX API URL, must be changed when we migrate it to KLIPY + const url = new URL('https://proxy.commet.chat'); + url.pathname = '/proxy/tenor/api/v2/search'; + url.searchParams.set('q', trimmedQuery || DEFAULT_GIF_QUERY); + + const response = await fetch(url.toString()); + + if (response.status === 200) { + const data = await response.json(); + const results = data.results as any[] | undefined; + + if (results) { + const gifData: GifData[] = results.map(parseTenorResult); + setGifs(gifData); + } else { + setGifs([]); + } + } else { + throw new Error(`HTTP ${response.status}`); + } + } catch { + setError('Failed to search GIFs'); + setGifs([]); + } finally { + setLoading(false); + } + }, + [parseTenorResult] + ); + + return { gifs, loading, error, searchGifs }; + } + + const { gifs, searchGifs } = useGifSearch(); + const [emojiGroupItems, stickerGroupItems, gifGroupItems] = useGroups(tab, imagePacks, gifs); + const groupsByTab = { + [EmojiBoardTab.Emoji]: emojiGroupItems, + [EmojiBoardTab.Sticker]: stickerGroupItems, + [EmojiBoardTab.Gif]: gifGroupItems, + }; + const groups = groupsByTab[tab]; + const renderItem = useItemRenderer(tab, saveStickerEmojiBandwidth); + const handleOnChange: ChangeEventHandler = useDebounce( useCallback( (evt) => { const term = evt.target.value; - if (term) search(term); - else resetSearch(); + if (tab === EmojiBoardTab.Gif) { + searchGifs(term); + } else if (term) { + search(term); + } else { + resetSearch(); + } }, - [search, resetSearch] + [search, resetSearch, searchGifs, tab] ), { wait: 200 } ); @@ -490,6 +650,11 @@ export function EmojiBoard({ if (emojiInfo.type === EmojiType.Sticker) { onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode, emojiInfo.label); } + if (emojiInfo.type === EmojiType.Gif) { + const gifDataStr = targetEl.getAttribute('data-gif-data'); + const gifData = gifDataStr ? JSON.parse(gifDataStr) : null; + onGifSelect?.(gifData); + } if (!evt.altKey && !evt.shiftKey) requestClose(); }; @@ -531,6 +696,13 @@ export function EmojiBoard({ } }, [tab, virtualizer, groups.length]); + // Load default/trending GIFs when opening the GIF tab. + useEffect(() => { + if (gifTab) { + searchGifs(''); + } + }, [gifTab, searchGifs]); + return ( ) : ( - + !gifTab && ( + + ) ) } > @@ -584,7 +758,7 @@ export function EmojiBoard({ previewAtom={previewAtom} onGroupItemClick={handleGroupItemClick} > - {searchedItems && ( + {tab !== EmojiBoardTab.Gif && searchedItems && ( - + {group.items.map(renderItem)} @@ -619,7 +793,7 @@ export function EmojiBoard({ {tab === EmojiBoardTab.Sticker && groups.length === 0 && } - + {!gifTab && } ); diff --git a/src/app/components/emoji-board/components/Group.tsx b/src/app/components/emoji-board/components/Group.tsx index 293ac6145..90ddfe867 100644 --- a/src/app/components/emoji-board/components/Group.tsx +++ b/src/app/components/emoji-board/components/Group.tsx @@ -10,9 +10,10 @@ export const EmojiGroup = as< { id: string; label: string; + isGifGroup?: boolean; children: ReactNode; } ->(({ className, id, label, children, ...props }, ref) => ( +>(({ className, id, label, isGifGroup, children, ...props }, ref) => ( {label} -
- - {children} - +
+ {isGifGroup ? ( + children + ) : ( + + {children} + + )}
)); diff --git a/src/app/components/emoji-board/components/Item.tsx b/src/app/components/emoji-board/components/Item.tsx index c593b5db0..87c4cd885 100644 --- a/src/app/components/emoji-board/components/Item.tsx +++ b/src/app/components/emoji-board/components/Item.tsx @@ -3,7 +3,8 @@ import { MatrixClient } from '$types/matrix-sdk'; import { PackImageReader } from '$plugins/custom-emoji'; import { IEmoji } from '$plugins/emoji'; import { mxcUrlToHttp } from '$utils/matrix'; -import { EmojiItemInfo, EmojiType } from '$components/emoji-board/types'; +import { EmojiItemInfo, EmojiType, GifData } from '$components/emoji-board/types'; +import { CSSProperties, ReactNode } from 'react'; import * as css from './styles.css'; const ANIMATED_MIME_TYPES = new Set(['image/gif', 'image/apng']); @@ -32,17 +33,17 @@ const getPackImageSrc = ( }; export const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => { - const label = element.getAttribute('title'); + const label = element.getAttribute('data-emoji-label') ?? element.getAttribute('title'); const type = element.getAttribute('data-emoji-type') as EmojiType | undefined; const data = element.getAttribute('data-emoji-data'); const shortcode = element.getAttribute('data-emoji-shortcode'); - if (type && data && shortcode && label) + if (type && data && shortcode) return { type, data, shortcode, - label, + label: label ?? shortcode, }; return undefined; }; @@ -59,6 +60,7 @@ export function EmojiItem({ emoji }: EmojiItemProps) { justifyContent="Center" className={css.EmojiItem} title={emoji.label} + data-emoji-label={emoji.label} aria-label={`${emoji.label} emoji`} data-emoji-type={EmojiType.Emoji} data-emoji-data={emoji.unicode} @@ -89,6 +91,7 @@ export function CustomEmojiItem({ justifyContent="Center" className={css.EmojiItem} title={image.body || image.shortcode} + data-emoji-label={image.body || image.shortcode} aria-label={`${image.body || image.shortcode} emoji`} data-emoji-type={EmojiType.CustomEmoji} data-emoji-data={image.url} @@ -125,6 +128,7 @@ export function StickerItem({ justifyContent="Center" className={css.StickerItem} title={image.body || image.shortcode} + data-emoji-label={image.body || image.shortcode} aria-label={`${image.body || image.shortcode} emoji`} data-emoji-type={EmojiType.Sticker} data-emoji-data={image.url} @@ -139,3 +143,43 @@ export function StickerItem({ ); } + +export function GifItem({ + label, + type, + data, + shortcode, + gif, + style, + showTitle, + children, +}: { + label: string; + type: EmojiType; + data: string; + shortcode: string; + gif?: GifData; + style?: CSSProperties; + showTitle?: boolean; + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/app/components/emoji-board/components/Layout.tsx b/src/app/components/emoji-board/components/Layout.tsx index 286006896..1252109c0 100644 --- a/src/app/components/emoji-board/components/Layout.tsx +++ b/src/app/components/emoji-board/components/Layout.tsx @@ -24,7 +24,11 @@ export const EmojiBoardLayout = as< {children} - - {sidebar} + {sidebar && ( + <> + + {sidebar} + + )} )); diff --git a/src/app/components/emoji-board/components/SearchInput.tsx b/src/app/components/emoji-board/components/SearchInput.tsx index c3c80e939..7fb78fc89 100644 --- a/src/app/components/emoji-board/components/SearchInput.tsx +++ b/src/app/components/emoji-board/components/SearchInput.tsx @@ -1,6 +1,7 @@ import { ChangeEventHandler, useRef } from 'react'; import { Input, Chip, Icon, Icons, Text } from 'folds'; import { mobileOrTablet } from '$utils/user-agent'; +import { EmojiBoardTab } from '../types'; type SearchInputProps = { query?: string; @@ -27,10 +28,12 @@ export function SearchInput({ ref={inputRef} variant="SurfaceVariant" size="400" - placeholder={allowTextCustomEmoji ? 'Search or Text Reaction ' : 'Search'} + placeholder={ + allowTextCustomEmoji && !EmojiBoardTab.Gif ? 'Search or Text Reaction ' : 'Search' + } maxLength={50} after={ - allowTextCustomEmoji && query ? ( + allowTextCustomEmoji && query && !EmojiBoardTab.Gif ? ( + onTabChange(EmojiBoardTab.Gif)} + > + + GIF + + ( mx.sendEvent(roomId, EventType.Sticker, content); }; + const handleGifSelect = async (gif: GifData) => { + // Download the GIF data + const response = await fetch(gif.url); + if (response.status !== 200) { + throw new Error(`Failed to fetch GIF: ${response.status}`); + } + + const data = await response.arrayBuffer(); + const uint8Array = new Uint8Array(data); + + // Create a File object for the GIF + const filename = `${gif.title}.gif`; + const file = new File([uint8Array], filename, { type: 'image/gif' }); + + // Upload to Matrix + const uploadResponse = await mx.uploadContent(file, { + name: filename, + type: 'image/gif', + }); + const mxcUrl = uploadResponse.content_uri; + + const content: StickerEventContent & ReplyEventContent & IContent = { + body: filename, + url: mxcUrl, + info: { + w: gif.width, + h: gif.height, + mimetype: 'image/gif', + size: data.byteLength, + }, + }; + + // Handle replies if there's a reply draft + if (replyDraft) { + content['m.relates_to'] = { + 'm.in_reply_to': { + event_id: replyDraft.eventId, + }, + }; + if (replyDraft.relation?.rel_type === RelationType.Thread) { + content['m.relates_to'].event_id = replyDraft.relation.event_id; + content['m.relates_to'].rel_type = RelationType.Thread; + content['m.relates_to'].is_falling_back = false; + } + } + + // Send the gif as sticker event. + await mx.sendEvent(roomId, EventType.Sticker, content); + setReplyDraft(undefined); + }; + return (
{selectedFiles.length > 0 && ( @@ -1463,6 +1514,7 @@ export const RoomInput = forwardRef( onEmojiSelect={handleEmoticonSelect} onCustomEmojiSelect={handleEmoticonSelect} onStickerSelect={handleStickerSelect} + onGifSelect={handleGifSelect} requestClose={() => { setEmojiBoardTab((t) => { if (t) { @@ -1475,6 +1527,17 @@ export const RoomInput = forwardRef( /> } > + setEmojiBoardTab(EmojiBoardTab.Gif)} + variant="SurfaceVariant" + size="300" + radii="300" + > + {/* TODO: change the icon to a GIF icon, view https://github.com/cinnyapp/cinny/pull/2392 */} + + + {!hideStickerBtn && ( ( setEmojiBoardTab(EmojiBoardTab.Emoji)} variant="SurfaceVariant" @@ -1506,7 +1572,10 @@ export const RoomInput = forwardRef( From 1294244ae501c482e71073e5ec2b5a25e8841b65 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Sat, 11 Apr 2026 19:44:53 +0100 Subject: [PATCH 02/10] remove some vibe coded things, and undo the change that populated the page, since that may not be needed? --- src/app/components/emoji-board/EmojiBoard.tsx | 11 +---------- src/app/components/emoji-board/components/Item.tsx | 6 +----- src/app/components/emoji-board/components/Layout.tsx | 8 ++------ .../components/emoji-board/components/styles.css.ts | 5 ++--- 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index ec5740ae0..bb4054b33 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -423,8 +423,6 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = { }; const VIRTUAL_OVER_SCAN = 2; -// TODO: This should stop existing if the API provides a way to get trending GIFs -const DEFAULT_GIF_QUERY = 'trending'; type EmojiBoardProps = { tab?: EmojiBoardTab; @@ -545,7 +543,7 @@ export function EmojiBoard({ // TODO: FIX API URL, must be changed when we migrate it to KLIPY const url = new URL('https://proxy.commet.chat'); url.pathname = '/proxy/tenor/api/v2/search'; - url.searchParams.set('q', trimmedQuery || DEFAULT_GIF_QUERY); + url.searchParams.set('q', trimmedQuery); const response = await fetch(url.toString()); @@ -696,13 +694,6 @@ export function EmojiBoard({ } }, [tab, virtualizer, groups.length]); - // Load default/trending GIFs when opening the GIF tab. - useEffect(() => { - if (gifTab) { - searchGifs(''); - } - }, [gifTab, searchGifs]); - return ( { - const label = element.getAttribute('data-emoji-label') ?? element.getAttribute('title'); + const label = element.getAttribute('title'); const type = element.getAttribute('data-emoji-type') as EmojiType | undefined; const data = element.getAttribute('data-emoji-data'); const shortcode = element.getAttribute('data-emoji-shortcode'); @@ -60,7 +60,6 @@ export function EmojiItem({ emoji }: EmojiItemProps) { justifyContent="Center" className={css.EmojiItem} title={emoji.label} - data-emoji-label={emoji.label} aria-label={`${emoji.label} emoji`} data-emoji-type={EmojiType.Emoji} data-emoji-data={emoji.unicode} @@ -91,7 +90,6 @@ export function CustomEmojiItem({ justifyContent="Center" className={css.EmojiItem} title={image.body || image.shortcode} - data-emoji-label={image.body || image.shortcode} aria-label={`${image.body || image.shortcode} emoji`} data-emoji-type={EmojiType.CustomEmoji} data-emoji-data={image.url} @@ -128,7 +126,6 @@ export function StickerItem({ justifyContent="Center" className={css.StickerItem} title={image.body || image.shortcode} - data-emoji-label={image.body || image.shortcode} aria-label={`${image.body || image.shortcode} emoji`} data-emoji-type={EmojiType.Sticker} data-emoji-data={image.url} @@ -172,7 +169,6 @@ export function GifItem({ alignItems="Center" justifyContent="Center" title={showTitle ? label : undefined} - data-emoji-label={label} aria-label={`${label} gif`} data-emoji-type={type} data-emoji-data={data} diff --git a/src/app/components/emoji-board/components/Layout.tsx b/src/app/components/emoji-board/components/Layout.tsx index 1252109c0..286006896 100644 --- a/src/app/components/emoji-board/components/Layout.tsx +++ b/src/app/components/emoji-board/components/Layout.tsx @@ -24,11 +24,7 @@ export const EmojiBoardLayout = as< {children} - {sidebar && ( - <> - - {sidebar} - - )} + + {sidebar} )); diff --git a/src/app/components/emoji-board/components/styles.css.ts b/src/app/components/emoji-board/components/styles.css.ts index 75f2a6694..6a6b824d3 100644 --- a/src/app/components/emoji-board/components/styles.css.ts +++ b/src/app/components/emoji-board/components/styles.css.ts @@ -204,7 +204,6 @@ export const GifItem = style([ cursor: 'pointer', overflow: 'hidden', display: 'block', - backgroundColor: color.SurfaceVariant.Container, ':hover': { backgroundColor: color.Surface.ContainerHover, @@ -213,8 +212,8 @@ export const GifItem = style([ ]); export const GifImg = style({ - display: 'block', width: '100%', - height: 'auto', + height: '100%', objectFit: 'cover', + borderRadius: config.radii.R400, }); From 37aeda5170bda7c17dbe9000db42477b33b2e037 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Sat, 11 Apr 2026 19:45:56 +0100 Subject: [PATCH 03/10] fix: prevent searching for GIFs with empty term --- src/app/components/emoji-board/EmojiBoard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index bb4054b33..6c5c5e489 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -588,7 +588,9 @@ export function EmojiBoard({ (evt) => { const term = evt.target.value; if (tab === EmojiBoardTab.Gif) { - searchGifs(term); + if (term) { + searchGifs(term); + } } else if (term) { search(term); } else { From b377cd10a49f781ffe2ce7ebc22e2e97ad32e9f5 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Mon, 13 Apr 2026 11:30:14 +0100 Subject: [PATCH 04/10] feat: add GIF status handling and no results component --- .gitignore | 21 +++++++ src/app/components/emoji-board/EmojiBoard.tsx | 6 +- .../emoji-board/components/NoGifResults.tsx | 62 +++++++++++++++++++ .../emoji-board/components/index.tsx | 1 + 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/app/components/emoji-board/components/NoGifResults.tsx diff --git a/.gitignore b/.gitignore index d6c83cfb1..9c2ed5624 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,24 @@ build.sh # the following line was added with the "git ignore" tool by itsrye.dev, version 0.1.0 .lh + +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml + +# should not be done... but i dont want to add stuff that shouldnt be there +devenv.nix +devenv.yaml +.envrc +devenv.lock + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml +CLAUDE.md +.github/AGENTS.md +.gitignore +.github/copilot-instructions.md diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 6c5c5e489..15c1abbdf 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -42,6 +42,7 @@ import { SidebarDivider, Sidebar, NoStickerPacks, + GifStatus, createPreviewDataAtom, Preview, PreviewData, @@ -573,7 +574,7 @@ export function EmojiBoard({ return { gifs, loading, error, searchGifs }; } - const { gifs, searchGifs } = useGifSearch(); + const { gifs, loading: gifsLoading, error: gifsError, searchGifs } = useGifSearch(); const [emojiGroupItems, stickerGroupItems, gifGroupItems] = useGroups(tab, imagePacks, gifs); const groupsByTab = { [EmojiBoardTab.Emoji]: emojiGroupItems, @@ -784,6 +785,9 @@ export function EmojiBoard({ })}
{tab === EmojiBoardTab.Sticker && groups.length === 0 && } + {gifTab && ( + + )} {!gifTab && } diff --git a/src/app/components/emoji-board/components/NoGifResults.tsx b/src/app/components/emoji-board/components/NoGifResults.tsx new file mode 100644 index 000000000..2cdcb9d7d --- /dev/null +++ b/src/app/components/emoji-board/components/NoGifResults.tsx @@ -0,0 +1,62 @@ +import { Box, toRem, config, Icons, Icon, Text } from 'folds'; + +export function GifSearching() { + return ( + + Loading GIFs... + + ); +} + +export function GifSearchError({ error }: { error: string }) { + return ( + + Error: {error} + + ); +} + +export function NoGifResults() { + return ( + + + + No GIFs found! + + Try searching for something else. + + + + ); +} + +type GifStatusProps = { + loading: boolean; + error: string | null; + isEmpty: boolean; +}; + +export function GifStatus({ loading, error, isEmpty }: Readonly) { + if (loading) return ; + if (error) return ; + if (isEmpty) return ; + return null; +} diff --git a/src/app/components/emoji-board/components/index.tsx b/src/app/components/emoji-board/components/index.tsx index 555066684..d60160c83 100644 --- a/src/app/components/emoji-board/components/index.tsx +++ b/src/app/components/emoji-board/components/index.tsx @@ -2,6 +2,7 @@ export * from './SearchInput'; export * from './Tabs'; export * from './Sidebar'; export * from './NoStickerPacks'; +export * from './NoGifResults'; export * from './Preview'; export * from './Item'; export * from './Group'; From f315e5105d6a3da0f55d390728cdde7b31655fe3 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Mon, 13 Apr 2026 11:36:23 +0100 Subject: [PATCH 05/10] undo changes to gitignore --- .gitignore | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/.gitignore b/.gitignore index 9c2ed5624..5a6c2a49e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,27 +20,3 @@ devAssets !*.tfbackend.example crash.log build.sh - -# the following line was added with the "git ignore" tool by itsrye.dev, version 0.1.0 -.lh - -# Devenv -.devenv* -devenv.local.nix -devenv.local.yaml - -# should not be done... but i dont want to add stuff that shouldnt be there -devenv.nix -devenv.yaml -.envrc -devenv.lock - -# direnv -.direnv - -# pre-commit -.pre-commit-config.yaml -CLAUDE.md -.github/AGENTS.md -.gitignore -.github/copilot-instructions.md From eb239c3e30a22d70cfa6b24192c13af44e351046 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Mon, 13 Apr 2026 11:40:24 +0100 Subject: [PATCH 06/10] undo unecessary change --- src/app/components/emoji-board/EmojiBoard.tsx | 1 - src/app/components/emoji-board/components/Item.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 15c1abbdf..0f239961a 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -141,7 +141,6 @@ const useGroups = ( return g; }, [mx, imagePacks, tab]); - // TODO: verify this implementation const gifGroupItems = useMemo(() => { if (tab !== EmojiBoardTab.Gif) return []; return [ diff --git a/src/app/components/emoji-board/components/Item.tsx b/src/app/components/emoji-board/components/Item.tsx index 436c67403..7a356a5f0 100644 --- a/src/app/components/emoji-board/components/Item.tsx +++ b/src/app/components/emoji-board/components/Item.tsx @@ -38,12 +38,12 @@ export const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => const data = element.getAttribute('data-emoji-data'); const shortcode = element.getAttribute('data-emoji-shortcode'); - if (type && data && shortcode) + if (type && data && shortcode && label) return { type, data, shortcode, - label: label ?? shortcode, + label, }; return undefined; }; From 4f1b87a78a279d277206bc7c99b34c7cce441467 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Mon, 13 Apr 2026 13:13:52 +0100 Subject: [PATCH 07/10] fix: bug with tooltips remaining the same when switching tabs --- .gitignore | 24 +++++++++++++++++++ src/app/components/emoji-board/EmojiBoard.tsx | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5a6c2a49e..9c2ed5624 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,27 @@ devAssets !*.tfbackend.example crash.log build.sh + +# the following line was added with the "git ignore" tool by itsrye.dev, version 0.1.0 +.lh + +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml + +# should not be done... but i dont want to add stuff that shouldnt be there +devenv.nix +devenv.yaml +.envrc +devenv.lock + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml +CLAUDE.md +.github/AGENTS.md +.gitignore +.github/copilot-instructions.md diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 0f239961a..a2835f46c 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -459,8 +459,8 @@ export function EmojiBoard({ const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker; const previewAtom = useMemo( - () => createPreviewDataAtom(emojiTab ? DefaultEmojiPreview : undefined), - [emojiTab] + () => createPreviewDataAtom(tab === EmojiBoardTab.Emoji ? DefaultEmojiPreview : undefined), + [tab] ); const activeGroupIdAtom = useMemo(() => atom(undefined), []); const setActiveGroupId = useSetAtom(activeGroupIdAtom); From 84a8d0a48b6ca3a96d4ce6cef5cba8a17a283586 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Mon, 13 Apr 2026 15:07:22 +0100 Subject: [PATCH 08/10] add changeset --- .changeset/added_a_gif_search_functionality.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/added_a_gif_search_functionality.md diff --git a/.changeset/added_a_gif_search_functionality.md b/.changeset/added_a_gif_search_functionality.md new file mode 100644 index 000000000..2b7b937e1 --- /dev/null +++ b/.changeset/added_a_gif_search_functionality.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +# Added a GIF search functionality From 368b598467f977f63b7cc236ea4f7bd1accdd0a9 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Tue, 14 Apr 2026 14:13:28 +0100 Subject: [PATCH 09/10] undo changes to .gitignore again --- .gitignore | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 9c2ed5624..2df8aa0ad 100644 --- a/.gitignore +++ b/.gitignore @@ -22,25 +22,4 @@ crash.log build.sh # the following line was added with the "git ignore" tool by itsrye.dev, version 0.1.0 -.lh - -# Devenv -.devenv* -devenv.local.nix -devenv.local.yaml - -# should not be done... but i dont want to add stuff that shouldnt be there -devenv.nix -devenv.yaml -.envrc -devenv.lock - -# direnv -.direnv - -# pre-commit -.pre-commit-config.yaml -CLAUDE.md -.github/AGENTS.md -.gitignore -.github/copilot-instructions.md +.lh \ No newline at end of file From a163a0deeff0e0e106b2dcb2b0f7e2d94dffad58 Mon Sep 17 00:00:00 2001 From: Mateus Pinho Date: Tue, 14 Apr 2026 14:14:13 +0100 Subject: [PATCH 10/10] fix: completely revert changes to .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2df8aa0ad..d6c83cfb1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ crash.log build.sh # the following line was added with the "git ignore" tool by itsrye.dev, version 0.1.0 -.lh \ No newline at end of file +.lh