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
This commit is contained in:
@@ -99,8 +99,8 @@ export function GifPicker({ open, onClose, onPick }: Props) {
|
||||
)}
|
||||
{error && (
|
||||
<p className="col-span-3 px-2 py-4 text-center text-xs text-rose-300">
|
||||
{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.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -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/<path>. 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<string, { url: string; dims?: [number, number] }>;
|
||||
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<string, string>): Promise<GifResult[]> {
|
||||
async function fetchGiphy(path: string, params: Record<string, string>): Promise<GifResult[]> {
|
||||
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<GifResult[]> {
|
||||
// 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 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<GifResult[]> {
|
||||
return fetchTenor('featured', { limit: '30', locale });
|
||||
export async function featuredGifs(_locale: string = 'de'): Promise<GifResult[]> {
|
||||
return fetchGiphy('trending', { limit: '30' });
|
||||
}
|
||||
|
||||
export function getRecentGifs(): GifResult[] {
|
||||
|
||||
Reference in New Issue
Block a user