web: add giphy provider for gif search
All checks were successful
CI / test (push) Successful in 21s

This commit is contained in:
2026-03-08 13:57:03 +03:00
parent b6175352d0
commit a9106b7fa3
2 changed files with 67 additions and 26 deletions

View File

@@ -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=

View File

@@ -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<string> {
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<Array<{ name: string; url: string }>> {
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) {