import { useEffect, useState } from 'react'; import { supabase } from './supabase'; export interface LinkPreview { url: string; title: string | null; description: string | null; imageUrl: string | null; siteName: string | null; ok: boolean; } // In-memory cache keyed by URL — avoids re-invoking the edge function for // the same URL within a session even if many bubbles reference it. const cache = new Map(); const inflight = new Map>(); async function loadPreview(url: string): Promise { if (cache.has(url)) return cache.get(url)!; const existing = inflight.get(url); if (existing) return existing; const p = (async () => { // First try the cache table directly (RLS allows authenticated reads). // Works offline-first if the server has already fetched this URL. // `link_previews` is a later migration — cast around stale generated types. const { data: cached } = await (supabase as unknown as { from: (t: string) => { select: (cols: string) => { eq: (col: string, val: string) => { maybeSingle: () => Promise<{ data: { url: string; title: string | null; description: string | null; image_url: string | null; site_name: string | null; ok: boolean; fetched_at: string; } | null; error: Error | null; }>; }; }; }; }) .from('link_previews') .select('url, title, description, image_url, site_name, ok, fetched_at') .eq('url', url) .maybeSingle(); if (cached && cached.ok) { const preview: LinkPreview = { url: cached.url, title: cached.title, description: cached.description, imageUrl: cached.image_url, siteName: cached.site_name, ok: cached.ok, }; cache.set(url, preview); return preview; } try { const { data, error } = await supabase.functions.invoke('og-preview', { body: { url }, }); if (error) throw error; const preview = data as LinkPreview | null; cache.set(url, preview && preview.ok ? preview : null); return cache.get(url) ?? null; } catch { cache.set(url, null); return null; } })(); inflight.set(url, p); try { return await p; } finally { inflight.delete(url); } } export function useLinkPreview(url: string | null): LinkPreview | null { const [preview, setPreview] = useState(() => url ? cache.get(url) ?? null : null, ); useEffect(() => { if (!url) { setPreview(null); return; } let cancelled = false; void loadPreview(url).then((p) => { if (!cancelled) setPreview(p); }); return () => { cancelled = true; }; }, [url]); return preview; } // Regex tuned for plain URLs inside message text. No markdown link syntax yet. const URL_RE = /https?:\/\/[^\s<>"']+/i; export function extractFirstUrl(text: string): string | null { const m = URL_RE.exec(text); return m?.[0] ?? null; }