mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Eliminate direct @plebbit/plebbit-js (8.7MB) imports from all UI components by replacing Plebbit.getShortAddress() with a local get-short-address utility. Add Vite manual chunks for plebbit-js, plebbit-react-hooks, and react-spring/use-gesture. Fix catalog CLS by matching skeleton to wrapper dimensions. Lazy-load ChallengeModal and ReplyModal. Add failed-URL cache to useFetchGifFirstFrame, optimize useHide selector, and memoize useFeed options in mod-queue.
116 lines
3.2 KiB
TypeScript
116 lines
3.2 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js';
|
|
|
|
const gifFrameDb = localForageLru.createInstance({ name: '5chanGifFrames', size: 500 });
|
|
const failedUrls = new Set<string>();
|
|
|
|
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();
|
|
await new Promise((resolve) => {
|
|
image.src = URL.createObjectURL(new Blob([buf]));
|
|
image.onload = resolve;
|
|
});
|
|
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 [frameUrl, setFrameUrl] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!url) {
|
|
setFrameUrl(null);
|
|
return;
|
|
}
|
|
|
|
let isActive = true;
|
|
|
|
const fetchFrame = async () => {
|
|
if (failedUrls.has(url)) {
|
|
if (isActive) setFrameUrl(null);
|
|
return;
|
|
}
|
|
try {
|
|
const cachedFrame = await getCachedGifFrame(url);
|
|
if (cachedFrame) {
|
|
try {
|
|
const response = await fetch(cachedFrame);
|
|
if (response.ok) {
|
|
if (isActive) setFrameUrl(cachedFrame);
|
|
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) {
|
|
setFrameUrl(objectUrl);
|
|
await setCachedGifFrame(url, objectUrl);
|
|
}
|
|
} catch (error) {
|
|
failedUrls.add(url);
|
|
console.error('Failed to load GIF frame:', error);
|
|
if (isActive) setFrameUrl(null);
|
|
}
|
|
};
|
|
|
|
fetchFrame();
|
|
|
|
return () => {
|
|
isActive = false;
|
|
};
|
|
}, [url]);
|
|
|
|
return frameUrl;
|
|
};
|
|
|
|
export default useFetchGifFirstFrame;
|