mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Distinguish GIF frame loading from GIF frame failure in useFetchGifFirstFrame(), so thread and catalog thumbnails no longer render animated GIFs before the still frame arrives. Keep the existing animated fallback when the first-frame fetch actually fails.
137 lines
4.0 KiB
TypeScript
137 lines
4.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import localForageLru from '@bitsocialhq/bitsocial-react-hooks/dist/lib/localforage-lru/index.js';
|
|
|
|
const gifFrameDb = localForageLru.createInstance({ name: '5chanGifFrames', size: 500 });
|
|
const failedUrls = new Set<string>();
|
|
|
|
type GifFirstFrameStatus = 'idle' | 'loading' | 'ready' | 'failed';
|
|
|
|
interface GifFirstFrameState {
|
|
frameUrl: string | null;
|
|
status: GifFirstFrameStatus;
|
|
}
|
|
|
|
const getCachedGifFrame = async (url: string): Promise<string | null> => {
|
|
return await gifFrameDb.getItem(url);
|
|
};
|
|
|
|
const setCachedGifFrame = async (url: string, frameUrl: string): Promise<void> => {
|
|
await gifFrameDb.setItem(url, frameUrl);
|
|
};
|
|
|
|
export const fetchImage = (url: string): Promise<ArrayBuffer> => {
|
|
return new Promise((resolve, reject) => {
|
|
const request = new XMLHttpRequest();
|
|
request.open('GET', url, true);
|
|
request.responseType = 'arraybuffer';
|
|
request.onloadend = () => {
|
|
if (request.response !== undefined && (request.status === 200 || request.status === 304)) {
|
|
resolve(request.response);
|
|
} else {
|
|
reject(new Error(`XMLHttpRequest, ${request.statusText}`));
|
|
}
|
|
};
|
|
request.send();
|
|
});
|
|
};
|
|
|
|
export const readImage = (file: File): Promise<ArrayBuffer> => {
|
|
return new Promise((resolve) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
resolve(reader.result as ArrayBuffer);
|
|
};
|
|
reader.readAsArrayBuffer(file);
|
|
});
|
|
};
|
|
|
|
const parseGif = async (buf: ArrayBuffer): Promise<Blob> => {
|
|
const image = new Image();
|
|
const sourceUrl = URL.createObjectURL(new Blob([buf]));
|
|
|
|
await new Promise((resolve, reject) => {
|
|
image.onload = () => {
|
|
URL.revokeObjectURL(sourceUrl);
|
|
resolve(undefined);
|
|
};
|
|
image.onerror = () => {
|
|
URL.revokeObjectURL(sourceUrl);
|
|
reject(new Error('Failed to parse GIF'));
|
|
};
|
|
image.src = sourceUrl;
|
|
});
|
|
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = image.width;
|
|
canvas.height = image.height;
|
|
const ctx = canvas.getContext('2d');
|
|
if (ctx === null) throw new Error('Canvas Context null');
|
|
ctx.drawImage(image, 0, 0, image.width, image.height);
|
|
return await new Promise((resolve, reject) =>
|
|
canvas.toBlob((blob) => {
|
|
if (blob === null) {
|
|
reject('Canvas Blob null');
|
|
} else {
|
|
resolve(blob);
|
|
}
|
|
}),
|
|
);
|
|
};
|
|
|
|
const useFetchGifFirstFrame = (url: string | undefined) => {
|
|
const [gifFirstFrame, setGifFirstFrame] = useState<GifFirstFrameState>({ frameUrl: null, status: 'idle' });
|
|
|
|
useEffect(() => {
|
|
if (!url) {
|
|
setGifFirstFrame({ frameUrl: null, status: 'idle' });
|
|
return;
|
|
}
|
|
|
|
let isActive = true;
|
|
setGifFirstFrame({ frameUrl: null, status: 'loading' });
|
|
|
|
const fetchFrame = async () => {
|
|
if (failedUrls.has(url)) {
|
|
if (isActive) setGifFirstFrame({ frameUrl: null, status: 'failed' });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const cachedFrame = await getCachedGifFrame(url);
|
|
if (cachedFrame) {
|
|
try {
|
|
const response = await fetch(cachedFrame);
|
|
if (response.ok) {
|
|
if (isActive) setGifFirstFrame({ frameUrl: cachedFrame, status: 'ready' });
|
|
return;
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
const blob = typeof url === 'string' ? await parseGif(await fetchImage(url)) : await parseGif(await readImage(url as File));
|
|
const objectUrl = URL.createObjectURL(blob);
|
|
if (isActive) {
|
|
setGifFirstFrame({ frameUrl: objectUrl, status: 'ready' });
|
|
await setCachedGifFrame(url, objectUrl);
|
|
} else {
|
|
URL.revokeObjectURL(objectUrl);
|
|
}
|
|
} catch (error) {
|
|
failedUrls.add(url);
|
|
console.error('Failed to load GIF frame:', error);
|
|
if (isActive) setGifFirstFrame({ frameUrl: null, status: 'failed' });
|
|
}
|
|
};
|
|
|
|
fetchFrame();
|
|
|
|
return () => {
|
|
isActive = false;
|
|
};
|
|
}, [url]);
|
|
|
|
return gifFirstFrame;
|
|
};
|
|
|
|
export default useFetchGifFirstFrame;
|