import { useCallback, useEffect, useRef, useState } from 'react'; import type { LocaleCode } from '../i18n/messages'; type Photo = { id: number; file_path?: string; thumbnail_path?: string; created_at?: string; session_id?: string | null; uploader_name?: string | null; }; type RawPhoto = Record; export function usePollGalleryDelta(token: string, locale: LocaleCode) { const [photos, setPhotos] = useState([]); const [loading, setLoading] = useState(true); const [newCount, setNewCount] = useState(0); const latestAt = useRef(null); const etagRef = useRef(null); const timer = useRef(null); const [visible, setVisible] = useState( typeof document !== 'undefined' ? document.visibilityState === 'visible' : true ); const fetchDelta = useCallback(async () => { if (!token) { setLoading(false); return; } try { const params = new URLSearchParams(); if (latestAt.current) { params.set('since', latestAt.current); } params.set('locale', locale); const headers: HeadersInit = { 'Cache-Control': 'no-store', 'X-Locale': locale, 'Accept': 'application/json', }; if (etagRef.current) { headers['If-None-Match'] = etagRef.current; } const res = await fetch(`/api/v1/events/${encodeURIComponent(token)}/photos?${params.toString()}`, { headers, }); if (res.status === 304) return; // No new content if (!res.ok) { console.warn(`Gallery API error: ${res.status} ${res.statusText}`); return; // Don't update state on error } const json = await res.json(); etagRef.current = res.headers.get('ETag'); // Handle different response formats const rawPhotos = Array.isArray(json.data) ? json.data : Array.isArray(json) ? json : json.photos || []; const newPhotos: Photo[] = rawPhotos.map((photo: RawPhoto) => ({ ...(photo as Photo), session_id: typeof photo.session_id === 'string' ? photo.session_id : (photo.guest_name as string | null) ?? null, uploader_name: typeof photo.uploader_name === 'string' ? (photo.uploader_name as string) : typeof photo.guest_name === 'string' ? (photo.guest_name as string) : null, })); if (newPhotos.length > 0) { const added = newPhotos.length; const hasBaseline = latestAt.current !== null; setPhotos((prev) => { if (hasBaseline) { // Delta mode: merge new photos with existing list by id const merged = [...newPhotos, ...prev]; const byId = new Map(); merged.forEach((photo) => byId.set(photo.id, photo)); return Array.from(byId.values()); } return newPhotos; }); if (hasBaseline && added > 0) { setNewCount((c) => c + added); } // Update latest timestamp if (json.latest_photo_at) { latestAt.current = json.latest_photo_at; } else if (newPhotos.length > 0) { // Fallback: use newest photo timestamp const newest = newPhotos.reduce((latest: number, photo: RawPhoto) => { const photoTime = new Date((photo.created_at as string | undefined) || (photo.created_at_timestamp as number | undefined) || 0).getTime(); return photoTime > latest ? photoTime : latest; }, 0); latestAt.current = new Date(newest).toISOString(); } } else if (latestAt.current) { // Delta mode but no new photos: keep existing photos console.log('No new photos, keeping existing gallery state'); // Don't update photos state } else { // Initial load with no photos setPhotos([]); } setLoading(false); } catch (error) { console.error('Gallery polling error:', error); setLoading(false); // Don't update state on error - keep previous photos } }, [locale, token]); useEffect(() => { const onVis = () => setVisible(document.visibilityState === 'visible'); document.addEventListener('visibilitychange', onVis); return () => document.removeEventListener('visibilitychange', onVis); }, []); useEffect(() => { if (!token) { setPhotos([]); setLoading(false); return; } setLoading(true); latestAt.current = null; etagRef.current = null; setPhotos([]); void fetchDelta(); if (timer.current) window.clearInterval(timer.current); // Poll less aggressively when hidden const interval = visible ? 30_000 : 90_000; timer.current = window.setInterval(() => { void fetchDelta(); }, interval); return () => { if (timer.current) window.clearInterval(timer.current); }; }, [token, visible, locale, fetchDelta]); const refreshNow = useCallback(async () => { if (!token) { return; } setLoading(true); latestAt.current = null; etagRef.current = null; setNewCount(0); setPhotos([]); await fetchDelta(); }, [fetchDelta, token]); function acknowledgeNew() { setNewCount(0); } return { loading, photos, newCount, acknowledgeNew, refreshNow }; }