mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(popular-posts): randomize popular thread board selection on mount
This commit is contained in:
@@ -69,6 +69,21 @@ const renderHook = async (addresses: string[], subplebbits: Array<unknown>) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetHookRoot = () => {
|
||||||
|
act(() => root.unmount());
|
||||||
|
container.remove();
|
||||||
|
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRandomSequence = (values: number[]) => {
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
return vi.spyOn(Math, 'random').mockImplementation(() => values[index++] ?? values.at(-1) ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
describe('usePopularPosts', () => {
|
describe('usePopularPosts', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -169,4 +184,28 @@ describe('usePopularPosts', () => {
|
|||||||
expect(latestValue.isLoading).toBe(true);
|
expect(latestValue.isLoading).toBe(true);
|
||||||
expect(latestValue.popularPosts).toEqual([]);
|
expect(latestValue.popularPosts).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reshuffles the selected boards on each mount while keeping one top thread per board', async () => {
|
||||||
|
const addresses = Array.from({ length: 10 }, (_, index) => `board-${index}.eth`);
|
||||||
|
const subplebbits = addresses.map((address, index) => createSubplebbit(address, [createPost(address, 'top', 30 - index), createPost(address, 'backup', 10 - index)]));
|
||||||
|
const keepOrderRandom = mockRandomSequence(Array.from({ length: addresses.length - 1 }, () => 0.999_999));
|
||||||
|
|
||||||
|
await renderHook(addresses, subplebbits);
|
||||||
|
|
||||||
|
expect(latestValue.isLoading).toBe(false);
|
||||||
|
expect(latestValue.popularPosts.map((post) => post.subplebbitAddress)).toEqual(addresses.slice(0, 8));
|
||||||
|
expect(latestValue.popularPosts.every((post) => post.cid.endsWith('-top'))).toBe(true);
|
||||||
|
|
||||||
|
keepOrderRandom.mockRestore();
|
||||||
|
resetHookRoot();
|
||||||
|
|
||||||
|
const rotateOrderRandom = mockRandomSequence(Array.from({ length: addresses.length - 1 }, () => 0));
|
||||||
|
await renderHook(addresses, subplebbits);
|
||||||
|
|
||||||
|
expect(latestValue.isLoading).toBe(false);
|
||||||
|
expect(latestValue.popularPosts.map((post) => post.subplebbitAddress)).toEqual(addresses.slice(1, 9));
|
||||||
|
expect(latestValue.popularPosts.every((post) => post.cid.endsWith('-top'))).toBe(true);
|
||||||
|
|
||||||
|
rotateOrderRandom.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ type PopularPostCandidate = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type CommittedPopularPosts = {
|
type CommittedPopularPosts = {
|
||||||
boardAddresses: Set<string>;
|
|
||||||
cids: Set<string>;
|
|
||||||
posts: Comment[];
|
posts: Comment[];
|
||||||
revealed: boolean;
|
revealed: boolean;
|
||||||
};
|
};
|
||||||
@@ -46,9 +44,20 @@ function isBoardStillLoading(subplebbit: Subplebbit | undefined, loadingStartTim
|
|||||||
return nowSeconds - loadingStartTimestamp < BOARD_LOADING_TIMEOUT_SECONDS;
|
return nowSeconds - loadingStartTimestamp < BOARD_LOADING_TIMEOUT_SECONDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shuffleBoardAddresses(boardAddresses: string[]): string[] {
|
||||||
|
const shuffledBoardAddresses = [...boardAddresses];
|
||||||
|
|
||||||
|
for (let index = shuffledBoardAddresses.length - 1; index > 0; index -= 1) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * (index + 1));
|
||||||
|
[shuffledBoardAddresses[index], shuffledBoardAddresses[randomIndex]] = [shuffledBoardAddresses[randomIndex], shuffledBoardAddresses[index]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return shuffledBoardAddresses;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ranked by time-decayed popularity so the box surfaces posts with
|
* Each board contributes at most one time-decayed popular thread, but the
|
||||||
* recent engagement rather than stale all-time reply leaders.
|
* board order is shuffled on mount so repeat visits surface different boards.
|
||||||
*
|
*
|
||||||
* The first revealed set is frozen until the user refreshes or changes
|
* The first revealed set is frozen until the user refreshes or changes
|
||||||
* the board filter, so threads never disappear during background loads.
|
* the board filter, so threads never disappear during background loads.
|
||||||
@@ -56,20 +65,18 @@ function isBoardStillLoading(subplebbit: Subplebbit | undefined, loadingStartTim
|
|||||||
const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitAddresses: string[]) => {
|
const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitAddresses: string[]) => {
|
||||||
const inputKey = [...subplebbitAddresses].sort().join(',');
|
const inputKey = [...subplebbitAddresses].sort().join(',');
|
||||||
const committedRef = useRef<CommittedPopularPosts>({
|
const committedRef = useRef<CommittedPopularPosts>({
|
||||||
boardAddresses: new Set(),
|
|
||||||
posts: [],
|
posts: [],
|
||||||
cids: new Set(),
|
|
||||||
revealed: false,
|
revealed: false,
|
||||||
});
|
});
|
||||||
const prevInputKeyRef = useRef('');
|
const prevInputKeyRef = useRef('');
|
||||||
|
const randomizedBoardAddressesRef = useRef<string[]>([]);
|
||||||
|
|
||||||
// Reset committed when the requested board set changes (e.g. NSFW filter toggle).
|
// Reset committed and reshuffle when the requested board set changes (e.g. NSFW filter toggle).
|
||||||
if (prevInputKeyRef.current !== inputKey) {
|
if (prevInputKeyRef.current !== inputKey) {
|
||||||
prevInputKeyRef.current = inputKey;
|
prevInputKeyRef.current = inputKey;
|
||||||
|
randomizedBoardAddressesRef.current = shuffleBoardAddresses(subplebbitAddresses);
|
||||||
committedRef.current = {
|
committedRef.current = {
|
||||||
boardAddresses: new Set(),
|
|
||||||
posts: [],
|
posts: [],
|
||||||
cids: new Set(),
|
|
||||||
revealed: false,
|
revealed: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -86,9 +93,10 @@ const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitA
|
|||||||
try {
|
try {
|
||||||
const selectedLinks = new Set<string>();
|
const selectedLinks = new Set<string>();
|
||||||
const allPosts: PopularPostCandidate[] = [];
|
const allPosts: PopularPostCandidate[] = [];
|
||||||
|
const subplebbitsByAddress = new Map(subplebbitAddresses.map((boardAddress, index) => [boardAddress, subplebbits[index]]));
|
||||||
|
|
||||||
subplebbitAddresses.forEach((boardAddress, index) => {
|
randomizedBoardAddressesRef.current.forEach((boardAddress) => {
|
||||||
const subplebbit = subplebbits[index];
|
const subplebbit = subplebbitsByAddress.get(boardAddress);
|
||||||
if (!boardAddress || !subplebbit?.posts?.pages?.hot?.comments) {
|
if (!boardAddress || !subplebbit?.posts?.pages?.hot?.comments) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -118,8 +126,6 @@ const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitA
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
allPosts.sort((a, b) => popularityScore(b.post, nowSeconds) - popularityScore(a.post, nowSeconds));
|
|
||||||
|
|
||||||
return allPosts;
|
return allPosts;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in usePopularPosts:', err);
|
console.error('Error in usePopularPosts:', err);
|
||||||
@@ -127,28 +133,16 @@ const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitA
|
|||||||
}
|
}
|
||||||
}, [nowSeconds, subplebbits, subplebbitAddresses]);
|
}, [nowSeconds, subplebbits, subplebbitAddresses]);
|
||||||
|
|
||||||
const { boardAddresses, posts, cids } = committedRef.current;
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (posts.length >= MAX_POSTS || committedRef.current.revealed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { boardAddress, post } = candidate;
|
|
||||||
if (post.cid && !cids.has(post.cid) && !boardAddresses.has(boardAddress)) {
|
|
||||||
posts.push(post);
|
|
||||||
cids.add(post.cid);
|
|
||||||
boardAddresses.add(boardAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasPendingBoards = subplebbitAddresses.some((_, index) => isBoardStillLoading(subplebbits[index], loadingStartTimestamps[index], nowSeconds));
|
const hasPendingBoards = subplebbitAddresses.some((_, index) => isBoardStillLoading(subplebbits[index], loadingStartTimestamps[index], nowSeconds));
|
||||||
if (!committedRef.current.revealed && (posts.length >= MAX_POSTS || (!hasPendingBoards && posts.length > 0))) {
|
|
||||||
|
if (!committedRef.current.revealed && (candidates.length >= MAX_POSTS || (!hasPendingBoards && candidates.length > 0))) {
|
||||||
|
committedRef.current.posts = candidates.slice(0, MAX_POSTS).map(({ post }) => post);
|
||||||
committedRef.current.revealed = true;
|
committedRef.current.revealed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading = !committedRef.current.revealed;
|
const isLoading = !committedRef.current.revealed;
|
||||||
|
|
||||||
return { popularPosts: committedRef.current.revealed ? posts : [], isLoading, error: null as string | null };
|
return { popularPosts: committedRef.current.revealed ? committedRef.current.posts : [], isLoading, error: null as string | null };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default usePopularPosts;
|
export default usePopularPosts;
|
||||||
|
|||||||
Reference in New Issue
Block a user