mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix: scroll OP quote to thread card on same-route click
This commit is contained in:
@@ -76,6 +76,7 @@ const PostInfo = ({
|
|||||||
onApprove,
|
onApprove,
|
||||||
onReject,
|
onReject,
|
||||||
quotedByMap,
|
quotedByMap,
|
||||||
|
cidToReply,
|
||||||
}: PostProps) => {
|
}: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
|
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
|
||||||
@@ -438,6 +439,14 @@ const PostInfo = ({
|
|||||||
reply?.cid &&
|
reply?.cid &&
|
||||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||||
)}
|
)}
|
||||||
|
{cid &&
|
||||||
|
!parentCid &&
|
||||||
|
isInPostPageView &&
|
||||||
|
cidToReply &&
|
||||||
|
[...new Set<string>((post?.quotedCids || []) as string[])].map((quotedCid) => {
|
||||||
|
const reply = cidToReply.get(quotedCid);
|
||||||
|
return reply?.cid && !(reply.deleted || reply.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />;
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -681,6 +690,14 @@ const PostDesktop = ({
|
|||||||
return map;
|
return map;
|
||||||
}, [filteredReplies, numberToCid]);
|
}, [filteredReplies, numberToCid]);
|
||||||
|
|
||||||
|
const cidToReply = useMemo(() => {
|
||||||
|
const map = new Map<string, Comment>();
|
||||||
|
for (const reply of filteredReplies) {
|
||||||
|
if (reply.cid) map.set(reply.cid, reply);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [filteredReplies]);
|
||||||
|
|
||||||
// Virtuoso scroll position management for infinite replies
|
// Virtuoso scroll position management for infinite replies
|
||||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||||
const virtuosoStateKey = `replies-desktop-${cid}`;
|
const virtuosoStateKey = `replies-desktop-${cid}`;
|
||||||
@@ -769,6 +786,7 @@ const PostDesktop = ({
|
|||||||
isPublishing={isPublishing}
|
isPublishing={isPublishing}
|
||||||
onApprove={onApprove}
|
onApprove={onApprove}
|
||||||
onReject={onReject}
|
onReject={onReject}
|
||||||
|
cidToReply={cidToReply}
|
||||||
/>
|
/>
|
||||||
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
||||||
{!isHidden && <CommentContent comment={post} />}
|
{!isHidden && <CommentContent comment={post} />}
|
||||||
|
|||||||
@@ -380,32 +380,46 @@ const PostMediaContent = ({ post, link }: { post: any; link: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReplyBacklinks = ({ post, quotedByMap }: PostProps) => {
|
const ReplyBacklinks = ({ post, quotedByMap, cidToReply, isInPostPageView }: PostProps) => {
|
||||||
const { cid, parentCid } = post || {};
|
const { cid, parentCid } = post || {};
|
||||||
const { replies } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
|
const { replies } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
|
||||||
|
|
||||||
return (
|
const opBacklinks =
|
||||||
cid &&
|
cid &&
|
||||||
parentCid &&
|
!parentCid &&
|
||||||
(replies.length > 0 || quotedByMap?.get(cid)?.length) && (
|
isInPostPageView &&
|
||||||
<div className={styles.mobileReplyBacklinks}>
|
cidToReply &&
|
||||||
{replies.map(
|
post?.quotedCids?.length &&
|
||||||
|
[...new Set<string>(post.quotedCids as string[])]
|
||||||
|
.map((quotedCid) => {
|
||||||
|
const reply = cidToReply.get(quotedCid);
|
||||||
|
return reply?.cid && !(reply.deleted || reply.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const replyBacklinks = cid && parentCid && ((replies?.length || 0) > 0 || quotedByMap?.get(cid)?.length) && (
|
||||||
|
<>
|
||||||
|
{replies?.map(
|
||||||
|
(reply: Comment, index: number) =>
|
||||||
|
reply?.parentCid === cid && reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||||
|
)}
|
||||||
|
{quotedByMap
|
||||||
|
?.get(cid)
|
||||||
|
?.map(
|
||||||
(reply: Comment, index: number) =>
|
(reply: Comment, index: number) =>
|
||||||
reply?.parentCid === cid &&
|
reply?.parentCid !== cid &&
|
||||||
reply?.cid &&
|
reply?.cid &&
|
||||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||||
)}
|
)}
|
||||||
{quotedByMap
|
</>
|
||||||
?.get(cid)
|
|
||||||
?.map(
|
|
||||||
(reply: Comment, index: number) =>
|
|
||||||
reply?.parentCid !== cid &&
|
|
||||||
reply?.cid &&
|
|
||||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return opBacklinks?.length > 0 || replyBacklinks ? (
|
||||||
|
<div className={styles.mobileReplyBacklinks}>
|
||||||
|
{opBacklinks}
|
||||||
|
{replyBacklinks}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
|
const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
|
||||||
@@ -434,7 +448,7 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: Post
|
|||||||
>
|
>
|
||||||
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} threadNumber={threadNumber} />
|
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} threadNumber={threadNumber} />
|
||||||
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />}
|
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />}
|
||||||
<ReplyBacklinks post={reply} quotedByMap={quotedByMap} />
|
<ReplyBacklinks post={post} quotedByMap={quotedByMap} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -520,6 +534,14 @@ const PostMobile = ({
|
|||||||
return map;
|
return map;
|
||||||
}, [filteredReplies, numberToCid]);
|
}, [filteredReplies, numberToCid]);
|
||||||
|
|
||||||
|
const cidToReply = useMemo(() => {
|
||||||
|
const map = new Map<string, Comment>();
|
||||||
|
for (const reply of filteredReplies) {
|
||||||
|
if (reply.cid) map.set(reply.cid, reply);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [filteredReplies]);
|
||||||
|
|
||||||
// Virtuoso scroll position management for infinite replies
|
// Virtuoso scroll position management for infinite replies
|
||||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||||
const virtuosoStateKey = `replies-mobile-${cid}`;
|
const virtuosoStateKey = `replies-mobile-${cid}`;
|
||||||
@@ -588,6 +610,7 @@ const PostMobile = ({
|
|||||||
{shouldShowSnow() && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
{shouldShowSnow() && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
||||||
<PostInfoAndMedia post={post} postReplyCount={replyCount} roles={roles} threadNumber={post?.number} />
|
<PostInfoAndMedia post={post} postReplyCount={replyCount} roles={roles} threadNumber={post?.number} />
|
||||||
<CommentContent comment={post} />
|
<CommentContent comment={post} />
|
||||||
|
<ReplyBacklinks post={post} quotedByMap={quotedByMap} cidToReply={cidToReply} isInPostPageView={isInPostView} />
|
||||||
</div>
|
</div>
|
||||||
{!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
|
{!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
|
||||||
<div className={styles.postLink}>
|
<div className={styles.postLink}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Comment, useAccount } from '@plebbit/plebbit-react-hooks';
|
import { Comment, useAccount } from '@plebbit/plebbit-react-hooks';
|
||||||
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
|
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
|
||||||
import { useDirectories } from '../../hooks/use-directories';
|
import { useDirectories } from '../../hooks/use-directories';
|
||||||
@@ -63,6 +63,13 @@ const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrollToThreadCardTop = (threadCid: string) => {
|
||||||
|
const threadCard = document.querySelector<HTMLElement>(`[data-cid="${threadCid}"][data-post-cid="${threadCid}"]`);
|
||||||
|
if (!threadCard) return false;
|
||||||
|
threadCard.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP }: ReplyQuotePreviewProps) => {
|
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP }: ReplyQuotePreviewProps) => {
|
||||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||||
@@ -107,12 +114,18 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
|||||||
}, [update]);
|
}, [update]);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined) => {
|
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined, isOpQuote = false) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (cid && subplebbitAddress) {
|
if (cid && subplebbitAddress) {
|
||||||
const boardPath = getBoardPath(subplebbitAddress, directories);
|
const boardPath = getBoardPath(subplebbitAddress, directories);
|
||||||
navigate(`/${boardPath}/thread/${cid}`);
|
const threadRoute = `/${boardPath}/thread/${cid}`;
|
||||||
|
if (isOpQuote && location.pathname === threadRoute) {
|
||||||
|
scrollToThreadCardTop(cid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(threadRoute);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -175,7 +188,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
|||||||
className={styles.quoteLink}
|
className={styles.quoteLink}
|
||||||
onMouseOver={() => handleMouseOver(quotelinkReply?.cid)}
|
onMouseOver={() => handleMouseOver(quotelinkReply?.cid)}
|
||||||
onMouseLeave={() => handleMouseLeave(quotelinkReply?.cid)}
|
onMouseLeave={() => handleMouseLeave(quotelinkReply?.cid)}
|
||||||
onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress)}
|
onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress, !!isOP)}
|
||||||
>
|
>
|
||||||
{`>>${quotelinkReply?.number ?? '?'}`}
|
{`>>${quotelinkReply?.number ?? '?'}`}
|
||||||
{isOP && ' (OP)'}
|
{isOP && ' (OP)'}
|
||||||
@@ -217,12 +230,18 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
|||||||
}, [update]);
|
}, [update]);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined) => {
|
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined, isOpQuote = false) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (cid && subplebbitAddress) {
|
if (cid && subplebbitAddress) {
|
||||||
const boardPath = getBoardPath(subplebbitAddress, directories);
|
const boardPath = getBoardPath(subplebbitAddress, directories);
|
||||||
navigate(`/${boardPath}/thread/${cid}`);
|
const threadRoute = `/${boardPath}/thread/${cid}`;
|
||||||
|
if (isOpQuote && location.pathname === threadRoute) {
|
||||||
|
scrollToThreadCardTop(cid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(threadRoute);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,7 +319,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
|||||||
: `/thread/${quotelinkReply.cid}`
|
: `/thread/${quotelinkReply.cid}`
|
||||||
: '#';
|
: '#';
|
||||||
return (
|
return (
|
||||||
<Link className={styles.quoteLink} to={quotelinkRoute} onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress)}>
|
<Link className={styles.quoteLink} to={quotelinkRoute} onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress, !!isOP)}>
|
||||||
{' '}
|
{' '}
|
||||||
#
|
#
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export interface PostProps {
|
|||||||
onApprove?: () => void;
|
onApprove?: () => void;
|
||||||
onReject?: () => void;
|
onReject?: () => void;
|
||||||
quotedByMap?: Map<string, Comment[]>;
|
quotedByMap?: Map<string, Comment[]>;
|
||||||
|
cidToReply?: Map<string, Comment>;
|
||||||
|
isInPostPageView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Post = ({
|
export const Post = ({
|
||||||
|
|||||||
Reference in New Issue
Block a user