mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(codebase audit): preserve cleanup without regressions
Fix codebase audit regressions while preserving UI/UX behavior and adding review-driven hardening.
This commit is contained in:
@@ -47,29 +47,41 @@ describe('browser hooks', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('tracks the window width across resize events', () => {
|
||||
it('tracks the window width across resize events', async () => {
|
||||
expect(renderHookValue(() => useWindowWidth())).toBe(1024);
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
window.innerWidth = 480;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
|
||||
expect(latestValue).toBe(480);
|
||||
});
|
||||
|
||||
it('derives the mobile breakpoint from the current window width', () => {
|
||||
it('derives the mobile breakpoint from the current window width', async () => {
|
||||
renderHookValue(() => useIsMobile());
|
||||
expect(latestValue).toBe(false);
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
window.innerWidth = 639;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
|
||||
expect(latestValue).toBe(true);
|
||||
});
|
||||
|
||||
it('reads the current width when remounted after a resize with no subscribers', () => {
|
||||
expect(renderHookValue(() => useWindowWidth())).toBe(1024);
|
||||
|
||||
act(() => root.unmount());
|
||||
window.innerWidth = 480;
|
||||
root = createRoot(container);
|
||||
|
||||
expect(renderHookValue(() => useWindowWidth())).toBe(480);
|
||||
});
|
||||
|
||||
it('updates the current time on the configured interval', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
import { Comment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { flattenCommentsPages } from '@bitsocial/bitsocial-react-hooks/dist/lib/utils';
|
||||
|
||||
const useCountLinksInReplies = (comment: Comment, firstXReplies?: number) => {
|
||||
const useCountLinksInReplies = (comment: Comment | undefined, firstXReplies?: number) => {
|
||||
let linkCount = 0;
|
||||
const flattenedReplies = useMemo(() => flattenCommentsPages(comment?.replies), [comment?.replies]);
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ export const useCurrentTime = (updateIntervalSeconds: number | false = 60) => {
|
||||
|
||||
// Update periodically
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentTime(Date.now() / 1000);
|
||||
setCurrentTime((previousTime) => {
|
||||
const nextTime = Date.now() / 1000;
|
||||
return Math.floor(nextTime) === Math.floor(previousTime) ? previousTime : nextTime;
|
||||
});
|
||||
}, updateIntervalSeconds * 1000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import useWindowWidth from './use-window-width';
|
||||
import { useIsMobileBreakpoint } from './use-window-width';
|
||||
|
||||
const useIsMobile = () => {
|
||||
const windowWidth = useWindowWidth();
|
||||
return windowWidth < 640;
|
||||
return useIsMobileBreakpoint();
|
||||
};
|
||||
|
||||
export default useIsMobile;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Comment, usePublishComment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import usePublishPostStore from '../stores/use-publish-post-store';
|
||||
import useChallengesStore from '../stores/use-challenges-store';
|
||||
import usePublishAuthorDomainGuard, { getPublishAuthorDomainErrorMessage } from './use-publish-author-domain-guard';
|
||||
@@ -9,14 +10,16 @@ type UsePublishPostOptions = {
|
||||
};
|
||||
|
||||
const usePublishPost = ({ communityAddress }: UsePublishPostOptions) => {
|
||||
const { author, title, content, link, spoiler, publishCommentOptions } = usePublishPostStore((state) => ({
|
||||
author: state.author,
|
||||
title: state.title || undefined,
|
||||
content: state.content || undefined,
|
||||
link: state.link || undefined,
|
||||
spoiler: state.spoiler || false,
|
||||
publishCommentOptions: state.publishCommentOptions,
|
||||
}));
|
||||
const { author, title, content, link, spoiler, publishCommentOptions } = usePublishPostStore(
|
||||
useShallow((state) => ({
|
||||
author: state.author,
|
||||
title: state.title || undefined,
|
||||
content: state.content || undefined,
|
||||
link: state.link || undefined,
|
||||
spoiler: state.spoiler || false,
|
||||
publishCommentOptions: state.publishCommentOptions,
|
||||
})),
|
||||
);
|
||||
|
||||
const setPublishPostStore = usePublishPostStore((state) => state.setPublishPostStore);
|
||||
const resetPublishPostStore = usePublishPostStore((state) => state.resetPublishPostStore);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Comment, useAccount, usePublishComment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useDirectories } from './use-directories';
|
||||
import usePublishReplyStore from '../stores/use-publish-reply-store';
|
||||
import usePostNumberStore, { getScopedNumberToCidMap } from '../stores/use-post-number-store';
|
||||
@@ -22,13 +23,15 @@ const usePublishReply = ({ cid, communityAddress, postCid }: UsePublishReplyOpti
|
||||
const account = useAccount();
|
||||
const directories = useDirectories();
|
||||
|
||||
const { author, content, link, spoiler, publishCommentOptions } = usePublishReplyStore((state) => ({
|
||||
author: state.author[parentCid],
|
||||
content: state.content[parentCid] || undefined,
|
||||
link: state.link[parentCid] || undefined,
|
||||
spoiler: state.spoiler[parentCid] || false,
|
||||
publishCommentOptions: state.publishCommentOptions[parentCid],
|
||||
}));
|
||||
const { author, content, link, spoiler, publishCommentOptions } = usePublishReplyStore(
|
||||
useShallow((state) => ({
|
||||
author: state.author[parentCid],
|
||||
content: state.content[parentCid] || undefined,
|
||||
link: state.link[parentCid] || undefined,
|
||||
spoiler: state.spoiler[parentCid] || false,
|
||||
publishCommentOptions: state.publishCommentOptions[parentCid],
|
||||
})),
|
||||
);
|
||||
|
||||
const setPublishReplyStore = usePublishReplyStore((state) => state.setPublishReplyStore);
|
||||
const resetPublishReplyStore = usePublishReplyStore((state) => state.resetPublishReplyStore);
|
||||
|
||||
@@ -47,7 +47,7 @@ const sanitizeSingleFeedLoadingState = (stateString?: string): string | undefine
|
||||
.replace(/\bloading thread\b/g, 'loading board');
|
||||
};
|
||||
|
||||
const useStateString = (commentOrCommunity: CommentOrCommunity): string | undefined => {
|
||||
const useStateString = (commentOrCommunity: CommentOrCommunity | undefined): string | undefined => {
|
||||
const { states: rawStates } = useClientsStates({ comment: commentOrCommunity }) as { states: States };
|
||||
|
||||
const debouncedStates = useMemo(() => {
|
||||
|
||||
@@ -1,18 +1,61 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
const useWindowWidth = () => {
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const MOBILE_BREAKPOINT_WIDTH = 640;
|
||||
const SERVER_WIDTH = 1024;
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowWidth(window.innerWidth);
|
||||
}
|
||||
type Listener = () => void;
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
const listeners = new Set<Listener>();
|
||||
let windowWidth = typeof window === 'undefined' ? SERVER_WIDTH : window.innerWidth;
|
||||
let animationFrameId: number | null = null;
|
||||
|
||||
return windowWidth;
|
||||
const readWindowWidth = () => (typeof window === 'undefined' ? SERVER_WIDTH : window.innerWidth);
|
||||
|
||||
const emitIfChanged = () => {
|
||||
const nextWindowWidth = readWindowWidth();
|
||||
if (nextWindowWidth === windowWidth) return;
|
||||
|
||||
windowWidth = nextWindowWidth;
|
||||
listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
if (typeof window === 'undefined' || animationFrameId !== null) return;
|
||||
|
||||
animationFrameId = window.requestAnimationFrame(() => {
|
||||
animationFrameId = null;
|
||||
emitIfChanged();
|
||||
});
|
||||
};
|
||||
|
||||
const subscribe = (listener: Listener) => {
|
||||
listeners.add(listener);
|
||||
|
||||
if (typeof window !== 'undefined' && listeners.size === 1) {
|
||||
windowWidth = readWindowWidth();
|
||||
window.addEventListener('resize', handleResize, { passive: true });
|
||||
}
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
|
||||
if (typeof window !== 'undefined' && listeners.size === 0) {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (animationFrameId !== null) {
|
||||
window.cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getWindowWidthSnapshot = () => windowWidth;
|
||||
const getServerWindowWidthSnapshot = () => SERVER_WIDTH;
|
||||
const getIsMobileSnapshot = () => windowWidth < MOBILE_BREAKPOINT_WIDTH;
|
||||
const getServerIsMobileSnapshot = () => SERVER_WIDTH < MOBILE_BREAKPOINT_WIDTH;
|
||||
|
||||
const useWindowWidth = () => useSyncExternalStore(subscribe, getWindowWidthSnapshot, getServerWindowWidthSnapshot);
|
||||
|
||||
export const useIsMobileBreakpoint = () => useSyncExternalStore(subscribe, getIsMobileSnapshot, getServerIsMobileSnapshot);
|
||||
|
||||
export default useWindowWidth;
|
||||
|
||||
Reference in New Issue
Block a user