ef1f9f45d8
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
116 lines
3.6 KiB
TypeScript
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 */
|
|
}
|
|
}
|