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[] {