Files
ChatApp/apps/desktop/src/lib/tenor.ts
T
byGalax ef1f9f45d8 fix(desktop): swap GIF provider from Tenor (closed Jan 2026) to GIPHY
Google closed Tenor v2 to new API clients in Jan 2026, so the only
people who could use the picker were those with a pre-existing Google
Cloud Console key. Swapped to GIPHY's Developer API (still open, free
keys at https://developers.giphy.com/dashboard/).

- Env var renamed VITE_TENOR_API_KEY → VITE_GIPHY_API_KEY
- Endpoint, response mapping, error sentinel updated
- File still named tenor.ts for import-path stability — renaming
  later if it bothers anyone
- Public GifResult interface unchanged so the picker UI didn't need
  edits beyond the error-message switch
2026-05-16 18:25:18 +02:00

116 lines
3.6 KiB
TypeScript

// GIPHY v1 client — Google closed Tenor to new API clients in Jan 2026, so
// the picker uses GIPHY's Developer API instead. Provision a free key at
// https://developers.giphy.com/dashboard/ and expose it via the renderer
// env var VITE_GIPHY_API_KEY (e.g. in apps/desktop/.env.local). When unset,
// every call throws — the picker degrades to a friendly
// "GIFs sind nicht konfiguriert" message via the consumer's catch.
//
// The file is still named `tenor.ts` for import-path stability; rename later
// if it bothers anyone.
const ENDPOINT = 'https://api.giphy.com/v1/gifs';
const RECENT_KEY = 'chatapp.gifRecent.v1';
const RECENT_MAX = 24;
const API_KEY = (
(import.meta as unknown as { env?: { VITE_GIPHY_API_KEY?: string } }).env
?.VITE_GIPHY_API_KEY ?? ''
).trim();
export interface GifResult {
id: string;
// Animated full-size URL (typically <2 MB).
url: string;
// Small preview shown in the picker grid.
previewUrl: string;
width: number;
height: number;
description: string;
}
interface GiphyImage {
url?: string;
width?: string;
height?: string;
}
interface GiphyResult {
id: string;
title?: string;
images?: {
original?: GiphyImage;
fixed_height?: GiphyImage;
fixed_height_small?: GiphyImage;
preview_gif?: GiphyImage;
};
}
function mapResult(r: GiphyResult): GifResult | null {
const full = r.images?.original ?? r.images?.fixed_height;
const preview =
r.images?.fixed_height_small ?? r.images?.preview_gif ?? r.images?.fixed_height ?? full;
if (!full?.url || !preview?.url) return null;
return {
id: r.id,
url: full.url,
previewUrl: preview.url,
width: full.width ? Number(full.width) : 0,
height: full.height ? Number(full.height) : 0,
description: r.title ?? '',
};
}
async function fetchGiphy(path: string, params: Record<string, string>): Promise<GifResult[]> {
if (!API_KEY) {
throw new Error('giphy_api_key_missing');
}
const search = new URLSearchParams({ api_key: API_KEY, rating: 'pg-13', ...params });
const res = await fetch(`${ENDPOINT}/${path}?${search.toString()}`);
if (!res.ok) throw new Error('giphy http ' + res.status);
const json = (await res.json()) as { data?: GiphyResult[] };
return (json.data ?? []).map(mapResult).filter((x): x is GifResult => x !== null);
}
export function isGifProviderConfigured(): boolean {
return API_KEY.length > 0;
}
// Back-compat alias for the original Tenor function name. New code should
// use isGifProviderConfigured.
export const isTenorConfigured = isGifProviderConfigured;
export async function searchGifs(query: string, locale: string = 'de'): Promise<GifResult[]> {
if (!query.trim()) return featuredGifs(locale);
return fetchGiphy('search', { q: query, limit: '30', lang: locale });
}
export async function featuredGifs(_locale: string = 'de'): Promise<GifResult[]> {
return fetchGiphy('trending', { limit: '30' });
}
export function getRecentGifs(): GifResult[] {
try {
const raw = window.localStorage.getItem(RECENT_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed.filter(
(x): x is GifResult =>
x != null &&
typeof x === 'object' &&
typeof (x as GifResult).url === 'string',
);
} catch {
return [];
}
}
export function rememberRecentGif(gif: GifResult): void {
const current = getRecentGifs().filter((g) => g.id !== gif.id);
const next = [gif, ...current].slice(0, RECENT_MAX);
try {
window.localStorage.setItem(RECENT_KEY, JSON.stringify(next));
} catch {
/* quota */
}
}