mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(popular-threads): adaptive ranking with grow-only stability
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useHomeFiltersStore from '../../../stores/use-popular-threads-options-store';
|
||||
import styles from '../home.module.css';
|
||||
@@ -11,21 +11,37 @@ const BoxModal = () => {
|
||||
|
||||
const { showWorksafeContentOnly, setShowWorksafeContentOnly, showNsfwContentOnly, setShowNsfwContentOnly } = useHomeFiltersStore();
|
||||
|
||||
const handleClickOutside = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
useEffect(() => {
|
||||
if (!showFilterModal) return;
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (modalRef.current && !modalRef.current.contains(event.target as Node) && buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
|
||||
setShowFilterModal(false);
|
||||
}
|
||||
},
|
||||
[modalRef, buttonRef, setShowFilterModal],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [handleClickOutside]);
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [showFilterModal]);
|
||||
|
||||
const selectFilter = (filter: 'worksafe' | 'nsfw' | 'all') => {
|
||||
if (filter === 'worksafe') {
|
||||
if (showNsfwContentOnly) setShowNsfwContentOnly(false);
|
||||
setShowWorksafeContentOnly(!showWorksafeContentOnly);
|
||||
} else if (filter === 'nsfw') {
|
||||
if (showWorksafeContentOnly) setShowWorksafeContentOnly(false);
|
||||
setShowNsfwContentOnly(!showNsfwContentOnly);
|
||||
} else {
|
||||
setShowWorksafeContentOnly(false);
|
||||
setShowNsfwContentOnly(false);
|
||||
}
|
||||
setShowFilterModal(false);
|
||||
};
|
||||
|
||||
const handleKey = (e: React.KeyboardEvent, filter: 'worksafe' | 'nsfw' | 'all') => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
selectFilter(filter);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -46,70 +62,29 @@ const BoxModal = () => {
|
||||
{showFilterModal && (
|
||||
<div ref={modalRef} className={styles.filterModal}>
|
||||
<div
|
||||
className={`${styles.option} ${showWorksafeContentOnly && styles.selected}`}
|
||||
className={`${styles.option} ${showWorksafeContentOnly ? styles.selected : ''}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (showNsfwContentOnly) {
|
||||
setShowNsfwContentOnly(false);
|
||||
}
|
||||
setShowWorksafeContentOnly(!showWorksafeContentOnly);
|
||||
setShowFilterModal(false);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (showNsfwContentOnly) {
|
||||
setShowNsfwContentOnly(false);
|
||||
}
|
||||
setShowWorksafeContentOnly(!showWorksafeContentOnly);
|
||||
setShowFilterModal(false);
|
||||
}}
|
||||
onKeyDown={(e) => handleKey(e, 'worksafe')}
|
||||
onClick={() => selectFilter('worksafe')}
|
||||
>
|
||||
{t('show_worksafe_content_only')}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.option} ${showNsfwContentOnly && styles.selected}`}
|
||||
className={`${styles.option} ${showNsfwContentOnly ? styles.selected : ''}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (showWorksafeContentOnly) {
|
||||
setShowWorksafeContentOnly(false);
|
||||
}
|
||||
setShowNsfwContentOnly(!showNsfwContentOnly);
|
||||
setShowFilterModal(false);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (showWorksafeContentOnly) {
|
||||
setShowWorksafeContentOnly(false);
|
||||
}
|
||||
setShowNsfwContentOnly(!showNsfwContentOnly);
|
||||
setShowFilterModal(false);
|
||||
}}
|
||||
onKeyDown={(e) => handleKey(e, 'nsfw')}
|
||||
onClick={() => selectFilter('nsfw')}
|
||||
>
|
||||
{t('show_nsfw_content_only')}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.option} ${!showWorksafeContentOnly && !showNsfwContentOnly && styles.selected}`}
|
||||
className={`${styles.option} ${!showWorksafeContentOnly && !showNsfwContentOnly ? styles.selected : ''}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setShowWorksafeContentOnly(false);
|
||||
setShowNsfwContentOnly(false);
|
||||
setShowFilterModal(false);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
setShowWorksafeContentOnly(false);
|
||||
setShowNsfwContentOnly(false);
|
||||
setShowFilterModal(false);
|
||||
}}
|
||||
onKeyDown={(e) => handleKey(e, 'all')}
|
||||
onClick={() => selectFilter('all')}
|
||||
>
|
||||
{t('show_all_content')}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,8 @@ import { removeMarkdown } from '../../../lib/utils/post-utils';
|
||||
|
||||
interface PopularThreadProps {
|
||||
post: Comment;
|
||||
directories: DirectoryCommunity[];
|
||||
boardTitle: string;
|
||||
boardPath: string;
|
||||
}
|
||||
|
||||
export const ContentPreview = ({ content, maxLength = 99 }: { content: string; maxLength?: number }) => {
|
||||
@@ -25,17 +26,11 @@ export const ContentPreview = ({ content, maxLength = 99 }: { content: string; m
|
||||
return truncatedText;
|
||||
};
|
||||
|
||||
// Memoize to prevent rerenders when parent rerenders due to updatingState
|
||||
const PopularThreadCard = memo(
|
||||
({ post, directories }: PopularThreadProps) => {
|
||||
const { cid, content, link, linkHeight, linkWidth, subplebbitAddress, thumbnailUrl, title } = post || {};
|
||||
({ post, boardTitle, boardPath }: PopularThreadProps) => {
|
||||
const { cid, content, link, linkHeight, linkWidth, thumbnailUrl, title } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
|
||||
// Find the matching DirectoryCommunity entry and get its title
|
||||
const directoriesEntry = directories.find((ms) => ms?.address === subplebbitAddress);
|
||||
const boardTitle = directoriesEntry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : '';
|
||||
|
||||
return (
|
||||
<div className={styles.popularThread} key={cid}>
|
||||
<div className={styles.title}>{boardTitle}</div>
|
||||
@@ -56,35 +51,31 @@ const PopularThreadCard = memo(
|
||||
</div>
|
||||
);
|
||||
},
|
||||
// Custom equality: rerender if post.cid or directories entry title changes
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.post?.cid !== nextProps.post?.cid) return false;
|
||||
// Compare the relevant directories entry for this post's subplebbitAddress
|
||||
const prevEntry = prevProps.directories.find((ms) => ms?.address === prevProps.post?.subplebbitAddress);
|
||||
const nextEntry = nextProps.directories.find((ms) => ms?.address === nextProps.post?.subplebbitAddress);
|
||||
return prevEntry?.title === nextEntry?.title;
|
||||
},
|
||||
(prevProps, nextProps) => prevProps.post?.cid === nextProps.post?.cid && prevProps.boardTitle === nextProps.boardTitle,
|
||||
);
|
||||
|
||||
const PopularThreadsBox = ({ directories, subplebbits }: { directories: DirectoryCommunity[]; subplebbits: any }) => {
|
||||
const { t } = useTranslation();
|
||||
const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore();
|
||||
|
||||
const directoryByAddress = useMemo(() => new Map(directories.map((d) => [d.address, d])), [directories]);
|
||||
|
||||
const filteredSubplebbits = useMemo(() => {
|
||||
if (showWorksafeContentOnly) {
|
||||
return subplebbits.filter((sub: Subplebbit) => {
|
||||
const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
|
||||
return directoriesEntry ? !directoriesEntry.nsfw : true;
|
||||
const entry = directoryByAddress.get(sub?.address);
|
||||
return entry ? !entry.nsfw : true;
|
||||
});
|
||||
}
|
||||
if (showNsfwContentOnly) {
|
||||
return subplebbits.filter((sub: Subplebbit) => {
|
||||
const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
|
||||
return directoriesEntry ? directoriesEntry.nsfw : false;
|
||||
const entry = directoryByAddress.get(sub?.address);
|
||||
return entry ? entry.nsfw : false;
|
||||
});
|
||||
}
|
||||
return subplebbits;
|
||||
}, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, directories]);
|
||||
}, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, directoryByAddress]);
|
||||
|
||||
const { popularPosts } = usePopularPosts(filteredSubplebbits);
|
||||
const isLoading = popularPosts.length === 0;
|
||||
|
||||
@@ -98,7 +89,12 @@ const PopularThreadsBox = ({ directories, subplebbits }: { directories: Director
|
||||
{isLoading ? (
|
||||
<LoadingEllipsis string={t('loading')} />
|
||||
) : (
|
||||
popularPosts.map((post: any) => <PopularThreadCard key={post.cid} post={post} directories={directories} />)
|
||||
popularPosts.map((post: Comment) => {
|
||||
const entry = directoryByAddress.get(post.subplebbitAddress);
|
||||
const boardTitle = entry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
|
||||
const boardPath = post.subplebbitAddress ? getBoardPath(post.subplebbitAddress, directories) : '';
|
||||
return <PopularThreadCard key={post.cid} post={post} boardTitle={boardTitle} boardPath={boardPath} />;
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user