From ef1f9f45d8adf2c234a2617cc9cd5e6736e8a5d7 Mon Sep 17 00:00:00 2001 From: byGalax Date: Sat, 16 May 2026 18:25:18 +0200 Subject: [PATCH] fix(desktop): swap GIF provider from Tenor (closed Jan 2026) to GIPHY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/desktop/src/components/GifPicker.tsx | 4 +- apps/desktop/src/lib/tenor.ts | 83 +++++++++++++---------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/apps/desktop/src/components/GifPicker.tsx b/apps/desktop/src/components/GifPicker.tsx index ef11de0..e0d02be 100644 --- a/apps/desktop/src/components/GifPicker.tsx +++ b/apps/desktop/src/components/GifPicker.tsx @@ -99,8 +99,8 @@ export function GifPicker({ open, onClose, onPick }: Props) { )} {error && (

- {error === 'tenor_api_key_missing' - ? 'GIFs sind nicht konfiguriert (VITE_TENOR_API_KEY fehlt).' + {error === 'giphy_api_key_missing' + ? 'GIFs sind nicht konfiguriert (VITE_GIPHY_API_KEY fehlt).' : 'GIFs gerade nicht verfügbar.'}

)} diff --git a/apps/desktop/src/lib/tenor.ts b/apps/desktop/src/lib/tenor.ts index 3d6c88f..87c645d 100644 --- a/apps/desktop/src/lib/tenor.ts +++ b/apps/desktop/src/lib/tenor.ts @@ -1,20 +1,19 @@ -// Tenor v2 client — Google's free GIF API. Public read endpoints accept any -// `client_key` so we don't ship a per-user API key. Trending + Search both -// hit https://tenor.googleapis.com/v2/. Cache recent picks (last 24 -// URLs) in localStorage so the picker has a "Zuletzt" tab. +// 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://tenor.googleapis.com/v2'; -const CLIENT_KEY = 'netralax-chat'; +const ENDPOINT = 'https://api.giphy.com/v1/gifs'; const RECENT_KEY = 'chatapp.gifRecent.v1'; const RECENT_MAX = 24; -// Tenor requires a Google API key. Provision one at -// https://developers.google.com/tenor/guides/quickstart and expose it via -// the renderer env var VITE_TENOR_API_KEY (e.g. in apps/desktop/.env.local). -// When unset, every Tenor call throws — the picker degrades to a friendly -// "GIFs gerade nicht verfügbar" message via the consumer's catch. const API_KEY = ( - (import.meta as unknown as { env?: { VITE_TENOR_API_KEY?: string } }).env - ?.VITE_TENOR_API_KEY ?? '' + (import.meta as unknown as { env?: { VITE_GIPHY_API_KEY?: string } }).env + ?.VITE_GIPHY_API_KEY ?? '' ).trim(); export interface GifResult { @@ -28,48 +27,64 @@ export interface GifResult { description: string; } -interface TenorApiResult { - id: string; - content_description?: string; - media_formats?: Record; +interface GiphyImage { + url?: string; + width?: string; + height?: string; } -function mapResult(r: TenorApiResult): GifResult | null { - const full = r.media_formats?.gif ?? r.media_formats?.mediumgif ?? r.media_formats?.tinygif; - const preview = r.media_formats?.tinygif ?? r.media_formats?.gif; +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.dims?.[0] ?? 0, - height: full.dims?.[1] ?? 0, - description: r.content_description ?? '', + width: full.width ? Number(full.width) : 0, + height: full.height ? Number(full.height) : 0, + description: r.title ?? '', }; } -async function fetchTenor(path: string, params: Record): Promise { +async function fetchGiphy(path: string, params: Record): Promise { if (!API_KEY) { - throw new Error('tenor_api_key_missing'); + throw new Error('giphy_api_key_missing'); } - const search = new URLSearchParams({ key: API_KEY, client_key: CLIENT_KEY, ...params }); + 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('tenor http ' + res.status); - const json = (await res.json()) as { results?: TenorApiResult[] }; - return (json.results ?? []).map(mapResult).filter((x): x is GifResult => x !== null); + 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 isTenorConfigured(): boolean { +export function isGifProviderConfigured(): boolean { return API_KEY.length > 0; } -export async function searchGifs(query: string, locale: string = 'de_DE'): Promise { +// 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 { if (!query.trim()) return featuredGifs(locale); - return fetchTenor('search', { q: query, limit: '30', locale }); + return fetchGiphy('search', { q: query, limit: '30', lang: locale }); } -export async function featuredGifs(locale: string = 'de_DE'): Promise { - return fetchTenor('featured', { limit: '30', locale }); +export async function featuredGifs(_locale: string = 'de'): Promise { + return fetchGiphy('trending', { limit: '30' }); } export function getRecentGifs(): GifResult[] {