mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
refactor(react-doctor): raise score from 79 to 90
This commit is contained in:
@@ -16,6 +16,34 @@ import { isBoardView, isPostPageView } from '../../../lib/utils/view-utils';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { PostMenuProps } from '../../../lib/utils/post-menu-props';
|
||||
|
||||
async function copyShareLinkSafe(boardIdentifier: string, linkType: ShareLinkType, cid?: string): Promise<void> {
|
||||
try {
|
||||
if (linkType === 'thread' && cid) {
|
||||
await copyShareLinkToClipboard(boardIdentifier, linkType, cid);
|
||||
} else {
|
||||
await copyShareLinkToClipboard(boardIdentifier, linkType as Exclude<ShareLinkType, 'thread'>);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to copy share link', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyContentIdSafe(cid: string): Promise<void> {
|
||||
try {
|
||||
await copyToClipboard(cid);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy content id', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyUserIdSafe(address: string): Promise<void> {
|
||||
try {
|
||||
await copyToClipboard(address);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy user id', error);
|
||||
}
|
||||
}
|
||||
|
||||
type HideButtonProps = {
|
||||
cid?: string;
|
||||
isReply?: boolean;
|
||||
@@ -31,19 +59,19 @@ const CopyLinkButton = ({ cid, subplebbitAddress, linkType, onClose }: CopyLinkB
|
||||
const { t } = useTranslation();
|
||||
const directories = useDirectories();
|
||||
const boardIdentifier = getBoardPath(subplebbitAddress, directories);
|
||||
const handleClick = async () => {
|
||||
await copyShareLinkSafe(boardIdentifier, linkType, linkType === 'thread' ? cid : undefined);
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (linkType === 'thread') {
|
||||
await copyShareLinkToClipboard(boardIdentifier, linkType, cid);
|
||||
} else {
|
||||
await copyShareLinkToClipboard(boardIdentifier, linkType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to copy share link', error);
|
||||
} finally {
|
||||
onClose();
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -54,15 +82,19 @@ const CopyLinkButton = ({ cid, subplebbitAddress, linkType, onClose }: CopyLinkB
|
||||
|
||||
const CopyContentIdButton = ({ cid, onClose }: { cid: string; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const handleClick = async () => {
|
||||
await copyContentIdSafe(cid);
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
onClick={async () => {
|
||||
try {
|
||||
await copyToClipboard(cid);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy content id', error);
|
||||
} finally {
|
||||
onClose();
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -73,15 +105,19 @@ const CopyContentIdButton = ({ cid, onClose }: { cid: string; onClose: () => voi
|
||||
|
||||
const CopyUserIdButton = ({ address, onClose }: { address: string; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const handleClick = async () => {
|
||||
await copyUserIdSafe(address);
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
onClick={async () => {
|
||||
try {
|
||||
await copyToClipboard(address);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy user id', error);
|
||||
} finally {
|
||||
onClose();
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -93,7 +129,17 @@ const CopyUserIdButton = ({ address, onClose }: { address: string; onClose: () =
|
||||
const ImageSearchButtons = ({ url, onClose }: { url: string; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div onClick={onClose}>
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={onClose}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<a href={`https://lens.google.com/uploadbyurl?url=${url}`} target='_blank' rel='noreferrer'>
|
||||
<div className={styles.postMenuItem}>{t('search_image_on_google')}</div>
|
||||
</a>
|
||||
@@ -112,12 +158,21 @@ const HidePostButton = ({ cid, isReply, onClose, postCid }: HideButtonProps) =>
|
||||
const { hide, hidden, unhide } = useHide({ cid: cid || '' });
|
||||
const isInPostView = isPostPageView(useLocation().pathname, useParams());
|
||||
|
||||
const handleClick = () => {
|
||||
hidden ? unhide() : hide();
|
||||
onClose && onClose();
|
||||
};
|
||||
return (
|
||||
(!isInPostView || isReply) && (
|
||||
<div
|
||||
onClick={() => {
|
||||
hidden ? unhide() : hide();
|
||||
onClose && onClose();
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.postMenuItem}>
|
||||
@@ -169,7 +224,21 @@ const PostMenuMobile = ({ postMenu, editMenuPost }: PostMenuMobileProps) => {
|
||||
<>
|
||||
{!(deleted || removed) && (
|
||||
<>
|
||||
<span className={styles.postMenuBtn} title='Post menu' onClick={handleMenuClick} ref={refs.setReference} {...getReferenceProps()}>
|
||||
<span
|
||||
className={styles.postMenuBtn}
|
||||
title='Post menu'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={handleMenuClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleMenuClick();
|
||||
}
|
||||
}}
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
>
|
||||
...
|
||||
</span>
|
||||
{isMenuOpen &&
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
@@ -187,22 +187,20 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||
|
||||
const hasFailedState = state === 'failed';
|
||||
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
||||
const postMenuProps = selectPostMenuProps(post);
|
||||
|
||||
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
|
||||
const showUserID = pseudonymityMode !== 'per-reply';
|
||||
|
||||
const handleUserAddressClick = useAuthorAddressClick();
|
||||
const numberOfPostsByAuthor = useMemo(() => {
|
||||
const numberOfPostsByAuthor = (() => {
|
||||
if (!showUserID || deleted || removed || !shortAddress || !postCid || typeof document === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const domCount = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
||||
// DOM-based count can be 0 on initial mount (before commit) or when parent isn't in DOM yet (e.g. Virtuoso board feed).
|
||||
// The current post is always at least 1 when we're displaying it.
|
||||
return Math.max(domCount, 1);
|
||||
}, [showUserID, deleted, removed, shortAddress, postCid, postReplyCount]);
|
||||
})();
|
||||
|
||||
const userID = address && Plebbit.getShortAddress({ address }); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing
|
||||
const userIDBackgroundColor = hashStringToColor(userID);
|
||||
@@ -239,10 +237,9 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
displayName.length <= 20 ? (
|
||||
displayName
|
||||
) : (
|
||||
<Tooltip
|
||||
children={displayName.slice(0, 20) + '(...)'}
|
||||
content={displayName.length < 1000 ? displayName : displayName.slice(0, 1000) + `... ${t('display_name_too_long')}`}
|
||||
/>
|
||||
<Tooltip content={displayName.length < 1000 ? displayName : displayName.slice(0, 1000) + `... ${t('display_name_too_long')}`}>
|
||||
{displayName.slice(0, 20) + '(...)'}
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
capitalize(t('anonymous'))
|
||||
@@ -272,19 +269,26 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
<span className={styles.pendingCid}>{hasFailedState ? capitalize(t('failed')) : capitalize(t('pending'))}</span>
|
||||
) : (
|
||||
<Tooltip
|
||||
children={
|
||||
<span
|
||||
title={t('highlight_posts')}
|
||||
className={styles.userAddress}
|
||||
onClick={() => handleUserAddressClick(userID, postCid)}
|
||||
style={{ backgroundColor: userIDBackgroundColor, color: userIDTextColor }}
|
||||
>
|
||||
{formatUserIDForDisplay(userID)}
|
||||
</span>
|
||||
}
|
||||
content={`${numberOfPostsByAuthor === 1 ? t('1_post_by_this_id') : t('x_posts_by_this_id', { number: numberOfPostsByAuthor })}`}
|
||||
showTooltip={isInPostPageView || postReplyCount < 6}
|
||||
/>
|
||||
>
|
||||
<span
|
||||
title={t('highlight_posts')}
|
||||
className={styles.userAddress}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={() => handleUserAddressClick(userID, postCid)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleUserAddressClick(userID, postCid);
|
||||
}
|
||||
}}
|
||||
style={{ backgroundColor: userIDBackgroundColor, color: userIDTextColor }}
|
||||
>
|
||||
{formatUserIDForDisplay(userID)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
){' '}
|
||||
</>
|
||||
@@ -304,10 +308,9 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
{title.length <= 30 ? (
|
||||
<span className={styles.subject}>{title}</span>
|
||||
) : (
|
||||
<Tooltip
|
||||
children={<span className={styles.subject}>{title.slice(0, 30) + '(...)'}</span>}
|
||||
content={title.length < 1000 ? title : title.slice(0, 1000) + `... ${t('title_too_long')}`}
|
||||
/>
|
||||
<Tooltip content={title.length < 1000 ? title : title.slice(0, 1000) + `... ${t('title_too_long')}`}>
|
||||
<span className={styles.subject}>{title.slice(0, 30) + '(...)'}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
@@ -321,11 +324,15 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
)}
|
||||
{isInModQueueView && isOverThreshold ? (
|
||||
<>
|
||||
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} /> (
|
||||
<span className={styles.alert}>{getFormattedTimeAgo(timestamp)}</span>)
|
||||
<Tooltip content={getFormattedTimeAgo(timestamp)}>
|
||||
<span>{getFormattedDate(timestamp)}</span>
|
||||
</Tooltip>{' '}
|
||||
(<span className={styles.alert}>{getFormattedTimeAgo(timestamp)}</span>)
|
||||
</>
|
||||
) : (
|
||||
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />
|
||||
<Tooltip content={getFormattedTimeAgo(timestamp)}>
|
||||
<span>{getFormattedDate(timestamp)}</span>
|
||||
</Tooltip>
|
||||
)}{' '}
|
||||
{cid ? (
|
||||
<span className={styles.postNumLink}>
|
||||
@@ -337,7 +344,19 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
>
|
||||
No.
|
||||
</Link>
|
||||
<span className={styles.replyToPost} title={t('reply_to_post')} onMouseDown={onReplyModalClick}>
|
||||
<span
|
||||
className={styles.replyToPost}
|
||||
title={t('reply_to_post')}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onMouseDown={onReplyModalClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onReplyModalClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{post?.number || '?'}
|
||||
</span>
|
||||
</span>
|
||||
@@ -425,16 +444,18 @@ const ReplyBacklinks = ({ post, quotedByMap, directRepliesByParentCid }: ReplyBa
|
||||
const replyBacklinks = cid && parentCid && (directReplies.length > 0 || quotedByMap?.get(cid)?.length) && (
|
||||
<>
|
||||
{directReplies.map(
|
||||
(reply: Comment, index: number) =>
|
||||
reply?.parentCid === cid && reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
(reply: Comment) =>
|
||||
reply?.parentCid === cid &&
|
||||
reply?.cid &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={reply.cid} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
{quotedByMap
|
||||
?.get(cid)
|
||||
?.map(
|
||||
(reply: Comment, index: number) =>
|
||||
(reply: Comment) =>
|
||||
reply?.parentCid !== cid &&
|
||||
reply?.cid &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -564,9 +585,9 @@ const PostMobile = ({
|
||||
const hasFailedState = state === 'failed';
|
||||
|
||||
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
|
||||
const filteredReplies = useMemo(() => repliesForRender.filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [repliesForRender]);
|
||||
const filteredReplies = repliesForRender.filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount)));
|
||||
|
||||
const directRepliesByParentCid = useMemo(() => {
|
||||
const directRepliesByParentCid = (() => {
|
||||
const map = new Map<string, Comment[]>();
|
||||
for (const reply of filteredReplies) {
|
||||
const directParentCid = reply?.parentCid;
|
||||
@@ -579,7 +600,7 @@ const PostMobile = ({
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [filteredReplies]);
|
||||
})();
|
||||
|
||||
const quotedByMap = useQuotedByMap(filteredReplies);
|
||||
|
||||
@@ -629,7 +650,18 @@ const PostMobile = ({
|
||||
<>
|
||||
<hr className={styles.unhideButtonHr} />
|
||||
<span className={styles.mobileUnhideButton}>
|
||||
<span className='button' onClick={unhide}>
|
||||
<span
|
||||
className='button'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={unhide}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
unhide();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Show Hidden Thread
|
||||
</span>
|
||||
</span>
|
||||
@@ -724,8 +756,8 @@ const PostMobile = ({
|
||||
!isInPendingPostView &&
|
||||
showReplies &&
|
||||
!hasMore &&
|
||||
visibleReplies.map((reply, index) => (
|
||||
<div key={index} className={styles.replyContainer}>
|
||||
visibleReplies.map((reply) => (
|
||||
<div key={reply.cid} className={styles.replyContainer}>
|
||||
<Reply
|
||||
postReplyCount={replyCount}
|
||||
reply={reply}
|
||||
@@ -742,8 +774,8 @@ const PostMobile = ({
|
||||
!isInPendingPostView &&
|
||||
repliesForRender &&
|
||||
showReplies &&
|
||||
filteredReplies.slice(-BOARD_REPLIES_PREVIEW_VISIBLE_COUNT).map((reply, index) => (
|
||||
<div key={index} className={styles.replyContainer}>
|
||||
filteredReplies.slice(-BOARD_REPLIES_PREVIEW_VISIBLE_COUNT).map((reply) => (
|
||||
<div key={reply.cid} className={styles.replyContainer}>
|
||||
<Reply
|
||||
postReplyCount={replyCount}
|
||||
reply={reply}
|
||||
|
||||
Reference in New Issue
Block a user