diff --git a/web/.env.example b/web/.env.example index 67b3697..7dec733 100644 --- a/web/.env.example +++ b/web/.env.example @@ -3,3 +3,4 @@ VITE_WS_URL=ws://localhost:8000/api/v1/realtime/ws VITE_GIF_PROVIDER= VITE_TENOR_API_KEY= VITE_TENOR_CLIENT_KEY=benya_messenger_web +VITE_GIPHY_API_KEY= diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index 9ebdf7b..910d531 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -34,7 +34,10 @@ const GIF_FAVORITES_KEY = "bm_gif_favorites_v1"; const GIF_PROVIDER = (import.meta.env.VITE_GIF_PROVIDER ?? "").toLowerCase(); const TENOR_API_KEY = (import.meta.env.VITE_TENOR_API_KEY ?? "").trim(); const TENOR_CLIENT_KEY = (import.meta.env.VITE_TENOR_CLIENT_KEY ?? "benya_messenger_web").trim(); -const GIF_SEARCH_ENABLED = GIF_PROVIDER === "tenor" && TENOR_API_KEY.length > 0; +const GIPHY_API_KEY = (import.meta.env.VITE_GIPHY_API_KEY ?? "").trim(); +const GIF_SEARCH_ENABLED = + (GIF_PROVIDER === "tenor" && TENOR_API_KEY.length > 0) || + (GIF_PROVIDER === "giphy" && GIPHY_API_KEY.length > 0); function loadFavorites(key: string): Set { try { @@ -124,6 +127,67 @@ export function MessageComposer() { (activeChat.type !== "channel" || activeChat.my_role === "owner" || activeChat.my_role === "admin" || activeChat.is_saved) ); + async function searchGifs(term: string): Promise> { + if (GIF_PROVIDER === "tenor") { + 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("tenor search failed"); + } + const data = (await response.json()) as { + results?: Array<{ content_description?: string; media_formats?: { gif?: { url?: string } } }>; + }; + return ( + data.results + ?.map((item) => ({ + name: item.content_description || "GIF", + url: item.media_formats?.gif?.url || "", + })) + .filter((item) => item.url.length > 0) ?? [] + ); + } + + if (GIF_PROVIDER === "giphy") { + const params = new URLSearchParams({ + api_key: GIPHY_API_KEY, + q: term, + limit: "24", + rating: "pg-13", + lang: "en", + }); + const response = await fetch(`https://api.giphy.com/v1/gifs/search?${params.toString()}`); + if (!response.ok) { + throw new Error("giphy search failed"); + } + const data = (await response.json()) as { + data?: Array<{ + title?: string; + images?: { + fixed_height?: { url?: string }; + original?: { url?: string }; + }; + }>; + }; + return ( + data.data + ?.map((item) => ({ + name: item.title || "GIF", + url: item.images?.fixed_height?.url || item.images?.original?.url || "", + })) + .filter((item) => item.url.length > 0) ?? [] + ); + } + + return []; + } + useEffect(() => { recordingStateRef.current = recordingState; }, [recordingState]); @@ -339,32 +403,8 @@ export function MessageComposer() { setGifSearchError(null); 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) { - if (response.status === 400) { - throw new Error("GIF provider rejected the request. Configure your own API key."); - } - throw new Error("gif search failed"); - } - const data = (await response.json()) as { - results?: Array<{ content_description?: string; media_formats?: { gif?: { url?: string } } }>; - }; + const rows = await searchGifs(term); 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) {