Files
fotospiel-app/resources/js/guest/polling/usePollGalleryDelta.ts

104 lines
3.7 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
type Photo = { id: number; file_path?: string; thumbnail_path?: string; created_at?: string };
export function usePollGalleryDelta(slug: string) {
const [photos, setPhotos] = useState<Photo[]>([]);
const [loading, setLoading] = useState(true);
const [newCount, setNewCount] = useState(0);
const latestAt = useRef<string | null>(null);
const timer = useRef<number | null>(null);
const [visible, setVisible] = useState(
typeof document !== 'undefined' ? document.visibilityState === 'visible' : true
);
async function fetchDelta() {
try {
const qs = latestAt.current ? `?since=${encodeURIComponent(latestAt.current)}` : '';
const res = await fetch(`/api/v1/events/${encodeURIComponent(slug)}/photos${qs}`, {
headers: { 'Cache-Control': 'no-store' },
});
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();
// Handle different response formats
const newPhotos = Array.isArray(json.data) ? json.data :
Array.isArray(json) ? json :
json.photos || [];
if (newPhotos.length > 0) {
const added = newPhotos.length;
if (latestAt.current) {
// Delta mode: Add new photos to existing list
const merged = [...newPhotos, ...photos];
// Remove duplicates by ID
const uniquePhotos = merged.filter((photo, index, self) =>
index === self.findIndex(p => p.id === photo.id)
);
setPhotos(uniquePhotos);
if (added > 0) setNewCount((c) => c + added);
} else {
// Initial load: Set all photos
setPhotos(newPhotos);
}
// 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: any) => {
const photoTime = new Date(photo.created_at || photo.created_at_timestamp || 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
}
}
useEffect(() => {
const onVis = () => setVisible(document.visibilityState === 'visible');
document.addEventListener('visibilitychange', onVis);
return () => document.removeEventListener('visibilitychange', onVis);
}, []);
useEffect(() => {
setLoading(true);
latestAt.current = null;
setPhotos([]);
fetchDelta();
if (timer.current) window.clearInterval(timer.current);
// Poll less aggressively when hidden
const interval = visible ? 30_000 : 90_000;
timer.current = window.setInterval(fetchDelta, interval);
return () => {
if (timer.current) window.clearInterval(timer.current);
};
}, [slug, visible]);
function acknowledgeNew() { setNewCount(0); }
return { loading, photos, newCount, acknowledgeNew };
}