feat(favicon): swap tab favicon for SFW vs NSFW boards

This commit is contained in:
plebeius
2026-03-02 21:08:46 +08:00
parent ea0789faa1
commit 3bd1c0ad4f
4 changed files with 71 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

+16
View File
@@ -7,6 +7,7 @@ import { useResolvedSubplebbitAddress } from './use-resolved-subplebbit-address'
import { useAccountComment } from '@bitsocialhq/pkc-react-hooks'; import { useAccountComment } from '@bitsocialhq/pkc-react-hooks';
import useSpecialThemeStore from '../stores/use-special-theme-store'; import useSpecialThemeStore from '../stores/use-special-theme-store';
import { isChristmas } from '../lib/utils/time-utils'; import { isChristmas } from '../lib/utils/time-utils';
import { updateFavicon, isSfwBoard } from '../lib/update-favicon';
const themeClasses = ['yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon']; const themeClasses = ['yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'];
@@ -80,11 +81,26 @@ const useTheme = (): [string, (theme: string) => void] => {
return storedTheme || 'yotsuba'; return storedTheme || 'yotsuba';
}, [location.pathname, isEnabled, isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, directories, themes]); }, [location.pathname, isEnabled, isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, directories, themes]);
const sfw = isSfwBoard({
pathname: location.pathname,
isSpecialTheme: !!isEnabled,
isInAllView,
isInSubscriptionsView,
isInModView,
subplebbitAddress,
directories,
});
// Update DOM class when theme changes // Update DOM class when theme changes
useEffect(() => { useEffect(() => {
updateThemeClass(currentTheme); updateThemeClass(currentTheme);
}, [currentTheme]); }, [currentTheme]);
// Update favicon when SFW status changes (separate effect for independent lifecycle)
useEffect(() => {
updateFavicon(sfw);
}, [sfw]);
const setSubplebbitTheme = useCallback( const setSubplebbitTheme = useCallback(
async (newTheme: string) => { async (newTheme: string) => {
if (isInAllView || isInSubscriptionsView || isInModView) { if (isInAllView || isInSubscriptionsView || isInModView) {
+54
View File
@@ -0,0 +1,54 @@
const DEFAULT_FAVICON = '/favicon.ico';
const SFW_FAVICON = '/favicon2.ico';
let currentHref: string | null = null;
/**
* Swap the tab favicon between the default (NSFW/home) and SFW variants.
* Uses remove-and-recreate to bypass aggressive browser favicon caching.
*/
export const updateFavicon = (isSfw: boolean): void => {
const href = isSfw ? SFW_FAVICON : DEFAULT_FAVICON;
if (href === currentHref) return;
currentHref = href;
const existing = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (existing) existing.remove();
const link = document.createElement('link');
link.rel = 'icon';
link.href = href;
document.head.appendChild(link);
};
/**
* Determine whether the current navigation context is a SFW board.
*
* Pure function — no hooks, no side-effects, fully testable.
*/
export const isSfwBoard = ({
pathname,
isSpecialTheme,
isInAllView,
isInSubscriptionsView,
isInModView,
subplebbitAddress,
directories,
}: {
pathname: string;
isSpecialTheme: boolean;
isInAllView: boolean;
isInSubscriptionsView: boolean;
isInModView: boolean;
subplebbitAddress: string | undefined;
directories: { address: string; nsfw?: boolean }[];
}): boolean => {
if (pathname === '/' || pathname.startsWith('/rules')) return false;
if (isSpecialTheme) return false;
if (isInAllView || isInSubscriptionsView || isInModView) return false;
if (!subplebbitAddress) return false;
const entry = directories.find((d) => d.address === subplebbitAddress);
return !entry?.nsfw;
};
+1 -1
View File
@@ -39,7 +39,7 @@ export default defineConfig({
enabled: true, enabled: true,
type: 'module', type: 'module',
}, },
includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'], includeAssets: ['favicon.ico', 'favicon2.ico', 'robots.txt', 'apple-touch-icon.png'],
manifest: { manifest: {
name: '5chan', name: '5chan',
short_name: '5chan', short_name: '5chan',