diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index 8fc937d..3957373 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -26,7 +26,7 @@ Legend: 17. Voice Messages - `PARTIAL` (record/send/play/seek/speed; UX still being polished) 18. Circle Video Messages - `PARTIAL` (send/play present, recording UX basic) 19. Stickers - `PARTIAL` (web sticker picker with preset pack + favorites) -20. GIF - `PARTIAL` (web GIF picker with preset catalog + local search + favorites) +20. GIF - `PARTIAL` (web GIF picker with Tenor search + preset fallback + favorites) 21. Message History/Search - `DONE` (history/pagination/chat+global search) 22. Text Formatting - `PARTIAL` (bold/italic/underline/spoiler/mono/links supported; toolbar still evolving) 23. Groups - `PARTIAL` (create/add/remove/invite link; advanced moderation partial) diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index 6910324..70e69cc 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -31,6 +31,8 @@ const GIF_PRESETS: Array<{ name: string; url: string }> = [ const STICKER_FAVORITES_KEY = "bm_sticker_favorites_v1"; const GIF_FAVORITES_KEY = "bm_gif_favorites_v1"; +const TENOR_API_KEY = "LIVDSRZULELA"; +const TENOR_CLIENT_KEY = "benya_messenger_web"; function loadFavorites(key: string): Set { try { @@ -98,6 +100,8 @@ export function MessageComposer() { const [stickerTab, setStickerTab] = useState<"all" | "favorites">("all"); const [gifTab, setGifTab] = useState<"all" | "favorites">("all"); const [gifQuery, setGifQuery] = useState(""); + const [gifResults, setGifResults] = useState>([]); + const [gifLoading, setGifLoading] = useState(false); const [favoriteStickers, setFavoriteStickers] = useState>(() => loadFavorites(STICKER_FAVORITES_KEY)); const [favoriteGifs, setFavoriteGifs] = useState>(() => loadFavorites(GIF_FAVORITES_KEY)); const [captionDraft, setCaptionDraft] = useState(""); @@ -312,6 +316,59 @@ export function MessageComposer() { }); } + useEffect(() => { + const term = gifQuery.trim(); + if (!showGifMenu || term.length < 2) { + setGifResults([]); + setGifLoading(false); + return; + } + let cancelled = false; + const timer = window.setTimeout(() => { + setGifLoading(true); + void (async () => { + try { + const params = new URLSearchParams({ + q: term, + key: TENOR_API_KEY, + client_key: TENOR_CLIENT_KEY, + limit: "24", + media_filter: "gif", + contentfilter: "medium", + }); + const response = await fetch(`https://tenor.googleapis.com/v2/search?${params.toString()}`); + if (!response.ok) { + throw new Error("gif search failed"); + } + const data = (await response.json()) as { + results?: Array<{ content_description?: string; media_formats?: { gif?: { url?: string } } }>; + }; + if (cancelled) return; + const rows = + data.results + ?.map((item) => ({ + name: item.content_description || "GIF", + url: item.media_formats?.gif?.url || "", + })) + .filter((item) => item.url.length > 0) ?? []; + setGifResults(rows); + } catch { + if (!cancelled) { + setGifResults([]); + } + } finally { + if (!cancelled) { + setGifLoading(false); + } + } + })(); + }, 280); + return () => { + cancelled = true; + window.clearTimeout(timer); + }; + }, [gifQuery, showGifMenu]); + function onComposerKeyDown(event: KeyboardEvent) { if (event.key !== "Enter") { return; @@ -925,8 +982,8 @@ export function MessageComposer() {
- {GIF_PRESETS - .filter((item) => item.name.toLowerCase().includes(gifQuery.trim().toLowerCase())) + {(gifResults.length > 0 ? gifResults : GIF_PRESETS) + .filter((item) => item.name.toLowerCase().includes(gifQuery.trim().toLowerCase()) || gifResults.length > 0) .filter((item) => (gifTab === "favorites" ? favoriteGifs.has(item.url) : true)) .map((gif) => (
+ {gifLoading ?

Searching GIF...

: null} ) : null}