feat(desktop): Tenor v2 GIF search/featured/recent client
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
// 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.
|
||||
|
||||
const ENDPOINT = 'https://tenor.googleapis.com/v2';
|
||||
const CLIENT_KEY = 'netralax-chat';
|
||||
const RECENT_KEY = 'chatapp.gifRecent.v1';
|
||||
const RECENT_MAX = 24;
|
||||
// Tenor's public key for browser-side reads. Documented as "anonymous" and
|
||||
// usable without account binding. Rate-limited at ~3k/day per IP which is
|
||||
// plenty for a chat app's picker traffic.
|
||||
const PUBLIC_API_KEY = 'AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ';
|
||||
|
||||
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 TenorApiResult {
|
||||
id: string;
|
||||
content_description?: string;
|
||||
media_formats?: Record<string, { url: string; dims?: [number, number] }>;
|
||||
}
|
||||
|
||||
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;
|
||||
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 ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchTenor(path: string, params: Record<string, string>): Promise<GifResult[]> {
|
||||
const search = new URLSearchParams({ key: PUBLIC_API_KEY, client_key: CLIENT_KEY, ...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);
|
||||
}
|
||||
|
||||
export async function searchGifs(query: string, locale: string = 'de_DE'): Promise<GifResult[]> {
|
||||
if (!query.trim()) return featuredGifs(locale);
|
||||
return fetchTenor('search', { q: query, limit: '30', locale });
|
||||
}
|
||||
|
||||
export async function featuredGifs(locale: string = 'de_DE'): Promise<GifResult[]> {
|
||||
return fetchTenor('featured', { limit: '30', locale });
|
||||
}
|
||||
|
||||
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 */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user