Add lightbox retries and queue removal
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-05 17:42:44 +01:00
parent 4e0d156065
commit 5f75c7ca6a
10 changed files with 282 additions and 49 deletions

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Button } from '@tamagui/button';
import { Camera, ChevronLeft, ChevronRight, Heart, Share2, Sparkles, X } from 'lucide-react';
import { Camera, ChevronLeft, ChevronRight, Heart, Loader2, Share2, Sparkles, X } from 'lucide-react';
import AppShell from '../components/AppShell';
import PhotoFrameTile from '../components/PhotoFrameTile';
import ShareSheet from '../components/ShareSheet';
@@ -84,15 +84,20 @@ export default function GalleryScreen() {
const [likedIds, setLikedIds] = React.useState<Set<number>>(new Set());
const touchStartX = React.useRef<number | null>(null);
const fallbackAttemptedRef = React.useRef(false);
const pendingNotFoundRef = React.useRef(false);
const photosRef = React.useRef<GalleryTile[]>([]);
const galleryLoadingRef = React.useRef(Boolean(token));
React.useEffect(() => {
if (!token) {
setPhotos([]);
galleryLoadingRef.current = false;
return;
}
let active = true;
setLoading(true);
galleryLoadingRef.current = true;
fetchGallery(token, { limit: 18, locale })
.then((response) => {
@@ -133,6 +138,7 @@ export default function GalleryScreen() {
if (active) {
setLoading(false);
}
galleryLoadingRef.current = false;
});
return () => {
@@ -140,6 +146,10 @@ export default function GalleryScreen() {
};
}, [token, locale]);
React.useEffect(() => {
photosRef.current = photos;
}, [photos]);
const myPhotoIds = React.useMemo(() => {
try {
const raw = localStorage.getItem('my-photo-ids');
@@ -295,6 +305,7 @@ export default function GalleryScreen() {
setLightboxPhoto(null);
setLightboxLoading(false);
setLightboxError(null);
pendingNotFoundRef.current = false;
return;
}
@@ -304,32 +315,61 @@ export default function GalleryScreen() {
if (seed) {
setLightboxPhoto(seed);
}
const hasSeed = Boolean(seed);
if (hasSeed) {
setLightboxLoading(false);
return;
}
let active = true;
const maxRetryMs = 10_000;
const retryDelayMs = 1500;
let timedOut = false;
const timeoutId = window.setTimeout(() => {
timedOut = true;
}, maxRetryMs);
setLightboxLoading(true);
setLightboxError(null);
fetchPhoto(selectedPhotoId, locale)
.then((photo) => {
if (!active || !photo) return;
const mapped = mapFullPhoto(photo as Record<string, unknown>);
if (mapped) {
setLightboxPhoto(mapped);
setLikesById((prev) => ({ ...prev, [mapped.id]: mapped.likes }));
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const loadPhoto = async () => {
let lastError: unknown = null;
while (active && !timedOut) {
try {
const photo = await fetchPhoto(selectedPhotoId, locale);
if (!active) return;
if (photo) {
const mapped = mapFullPhoto(photo as Record<string, unknown>);
if (mapped) {
setLightboxPhoto(mapped);
setLikesById((prev) => ({ ...prev, [mapped.id]: mapped.likes }));
setLightboxLoading(false);
return;
}
}
lastError = { status: 404 };
} catch (error) {
console.error('Lightbox photo load failed', error);
lastError = error;
}
})
.catch((error) => {
console.error('Lightbox photo load failed', error);
if (!active) return;
setLightboxError(error?.status === 404 ? 'notFound' : 'loadFailed');
})
.finally(() => {
if (active) {
setLightboxLoading(false);
}
});
if (!active || timedOut) break;
await sleep(retryDelayMs);
}
if (!active) return;
const status = (lastError as { status?: number } | null)?.status;
setLightboxError(status === 404 ? 'notFound' : 'loadFailed');
setLightboxLoading(false);
};
void loadPhoto();
return () => {
active = false;
window.clearTimeout(timeoutId);
};
}, [lightboxOpen, lightboxSelected, locale, selectedPhotoId]);
@@ -351,6 +391,17 @@ export default function GalleryScreen() {
};
}, [lightboxOpen]);
React.useEffect(() => {
if (!pendingNotFoundRef.current) return;
if (loading || galleryLoadingRef.current) return;
if (lightboxSelected || photosRef.current.some((photo) => photo.id === selectedPhotoId)) {
pendingNotFoundRef.current = false;
return;
}
pendingNotFoundRef.current = false;
setLightboxError('notFound');
}, [lightboxSelected, loading]);
React.useEffect(() => {
if (!lightboxOpen || !lightboxError) {
return;
@@ -897,9 +948,20 @@ export default function GalleryScreen() {
</Button>
</YStack>
) : (
<Text fontSize="$2" color="$color" opacity={0.7}>
{lightboxLoading ? t('galleryPage.loading', 'Loading…') : t('lightbox.errors.notFound', 'Photo not found')}
</Text>
<YStack alignItems="center" gap="$2">
{lightboxLoading ? (
<>
<Loader2 size={24} className="animate-spin" color={isDark ? '#F8FAFF' : '#0F172A'} />
<Text fontSize="$2" color="$color" opacity={0.7}>
{t('galleryPage.loading', 'Loading…')}
</Text>
</>
) : (
<Text fontSize="$2" color="$color" opacity={0.7}>
{t('lightbox.errors.notFound', 'Photo not found')}
</Text>
)}
</YStack>
)}
</YStack>
<XStack