Files
5chan/src/hooks/use-directories.ts
T

257 lines
7.3 KiB
TypeScript
Raw Normal View History

2024-03-01 20:42:21 +01:00
import { useEffect, useMemo, useState } from 'react';
import directoriesData from '../data/5chan-directories.json';
2024-03-01 20:42:21 +01:00
export interface DirectoriesMetadata {
title: string;
description: string;
createdAt: number;
updatedAt: number;
}
export interface DirectoryCommunity {
2024-03-01 20:42:21 +01:00
title?: string;
address: string;
nsfw?: boolean;
2024-03-01 20:42:21 +01:00
}
export interface DirectoriesData {
title: string;
description: string;
createdAt: number;
updatedAt: number;
communities: DirectoryCommunity[];
}
export interface DirectoriesState {
communities: DirectoryCommunity[];
2025-02-20 15:51:29 +01:00
loading: boolean;
error: Error | null;
}
2026-02-11 21:27:18 +08:00
const GITHUB_URL = 'https://raw.githubusercontent.com/bitsocialhq/lists/master/5chan-directories.json';
const LOCALSTORAGE_KEY = '5chan-directories-cache';
const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-directories-cache-timestamp';
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
let cacheCommunities: DirectoryCommunity[] | null = null;
let cacheMetadata: DirectoriesMetadata | null = null;
2026-02-15 14:40:48 +08:00
let inFlightGitHubFetch: Promise<DirectoriesData> | null = null;
2024-03-01 20:42:21 +01:00
const getFromLocalStorage = (): DirectoriesData | null => {
try {
const cached = localStorage.getItem(LOCALSTORAGE_KEY);
const timestamp = localStorage.getItem(LOCALSTORAGE_TIMESTAMP_KEY);
if (cached && timestamp) {
const age = Date.now() - parseInt(timestamp, 10);
if (age < CACHE_MAX_AGE_MS) {
return JSON.parse(cached);
}
}
} catch (e) {
console.warn('Failed to read from localStorage:', e);
}
return null;
};
const saveToLocalStorage = (data: DirectoriesData) => {
try {
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(data));
localStorage.setItem(LOCALSTORAGE_TIMESTAMP_KEY, Date.now().toString());
} catch (e) {
console.warn('Failed to save to localStorage:', e);
}
};
2026-02-15 14:40:48 +08:00
const fetchDirectoriesFromGitHub = async (): Promise<DirectoriesData> => {
const response = await fetch(GITHUB_URL, { cache: 'no-cache' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
2026-02-15 14:40:48 +08:00
const data = await response.json();
// Save successful fetch to localStorage
saveToLocalStorage(data);
return data;
};
const fetchDirectoriesFromGitHubDeduped = async (): Promise<DirectoriesData> => {
if (!inFlightGitHubFetch) {
inFlightGitHubFetch = fetchDirectoriesFromGitHub().finally(() => {
inFlightGitHubFetch = null;
});
}
return inFlightGitHubFetch;
};
export const useDirectories = () => {
// Use vendored data as initial state to prevent theme flash on first load
// This ensures NSFW status is known synchronously before first render
const [state, setState] = useState<DirectoriesState>({
communities: (directoriesData as DirectoriesData).communities,
2025-02-20 15:51:29 +01:00
loading: true,
error: null,
});
2024-03-01 20:42:21 +01:00
useEffect(() => {
let isMounted = true;
2026-02-15 14:40:48 +08:00
const hydrateCommunities = (data: DirectoriesData) => {
cacheCommunities = data.communities;
if (isMounted) {
setState({
communities: data.communities,
loading: false,
error: null,
});
}
};
2024-03-01 20:42:21 +01:00
(async () => {
2026-02-15 14:40:48 +08:00
if (cacheCommunities) {
setState({
communities: cacheCommunities,
loading: false,
error: null,
});
} else {
// Check localStorage first
const cachedData = getFromLocalStorage();
if (cachedData) {
2026-02-15 14:40:48 +08:00
hydrateCommunities(cachedData);
}
2026-02-15 14:40:48 +08:00
}
2025-02-20 15:51:29 +01:00
2026-02-15 14:40:48 +08:00
try {
// Always attempt a background refresh from GitHub to pick up list updates
const directories = await fetchDirectoriesFromGitHubDeduped();
hydrateCommunities(directories);
2024-03-01 20:42:21 +01:00
} catch (e) {
2026-02-15 14:40:48 +08:00
console.warn('Failed to fetch directories from GitHub:', e);
// Only fall back if we don't already have memory/localStorage data
if (!cacheCommunities) {
hydrateCommunities(directoriesData as DirectoriesData);
}
2024-03-01 20:42:21 +01:00
}
})();
return () => {
isMounted = false;
};
2024-03-01 20:42:21 +01:00
}, []);
// Always prefer cacheCommunities (module-level, stable reference) when available
// Only use state.communities during initial load before cache is populated
// This ensures a stable reference for memoization in consuming hooks
return cacheCommunities || state.communities;
2025-02-20 15:51:29 +01:00
};
export const useDirectoriesState = () => {
// Use vendored data as fallback to prevent theme flash on first load
const [state, setState] = useState<DirectoriesState>({
communities: cacheCommunities || (directoriesData as DirectoriesData).communities,
loading: !cacheCommunities,
2025-02-20 15:51:29 +01:00
error: null,
});
useEffect(() => {
let isMounted = true;
2026-02-15 14:40:48 +08:00
const hydrateCommunities = (data: DirectoriesData) => {
cacheCommunities = data.communities;
if (isMounted) {
setState({
communities: data.communities,
loading: false,
error: null,
});
}
};
2025-02-20 15:51:29 +01:00
(async () => {
2026-02-15 14:40:48 +08:00
if (cacheCommunities) {
setState({
communities: cacheCommunities,
loading: false,
error: null,
});
} else {
// Check localStorage first
const cachedData = getFromLocalStorage();
if (cachedData) {
2026-02-15 14:40:48 +08:00
hydrateCommunities(cachedData);
}
2026-02-15 14:40:48 +08:00
}
2025-02-20 15:51:29 +01:00
2026-02-15 14:40:48 +08:00
try {
// Always attempt a background refresh from GitHub to pick up list updates
const directories = await fetchDirectoriesFromGitHubDeduped();
hydrateCommunities(directories);
2025-02-20 15:51:29 +01:00
} catch (e) {
2026-02-15 14:40:48 +08:00
console.warn('Failed to fetch directories from GitHub:', e);
// Only fall back if we don't already have memory/localStorage data
if (!cacheCommunities) {
hydrateCommunities(directoriesData as DirectoriesData);
}
2025-02-20 15:51:29 +01:00
}
})();
return () => {
isMounted = false;
};
2025-02-20 15:51:29 +01:00
}, []);
return state;
2024-03-01 20:42:21 +01:00
};
export const useDirectoryAddresses = () => {
const directories = useDirectories();
return useMemo(() => directories.map((community) => community.address), [directories]);
2024-03-01 20:42:21 +01:00
};
export const useDirectoriesMetadata = () => {
const [metadata, setMetadata] = useState<DirectoriesMetadata | null>(null);
useEffect(() => {
let isMounted = true;
2026-02-15 14:40:48 +08:00
const hydrateMetadata = (data: DirectoriesData) => {
const nextMetadata: DirectoriesMetadata = {
title: data.title,
description: data.description,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
};
cacheMetadata = nextMetadata;
if (isMounted) {
setMetadata(nextMetadata);
}
};
(async () => {
2026-02-15 14:40:48 +08:00
if (cacheMetadata) {
setMetadata(cacheMetadata);
} else {
// Check localStorage first
const cachedData = getFromLocalStorage();
if (cachedData) {
2026-02-15 14:40:48 +08:00
hydrateMetadata(cachedData);
}
2026-02-15 14:40:48 +08:00
}
2026-02-15 14:40:48 +08:00
try {
// Always attempt a background refresh from GitHub to pick up metadata updates
const directories = await fetchDirectoriesFromGitHubDeduped();
hydrateMetadata(directories);
} catch (e) {
2026-02-15 14:40:48 +08:00
console.warn('Failed to fetch directory metadata from GitHub:', e);
// Only fall back if we don't already have memory/localStorage data
if (!cacheMetadata) {
hydrateMetadata(directoriesData as DirectoriesData);
}
}
})();
return () => {
isMounted = false;
};
}, []);
return cacheMetadata || metadata;
};