fix(routes): prevent malformed routes and fix cache invalidation

This commit is contained in:
plebeius
2025-11-07 18:34:06 +01:00
parent 669bb5b181
commit 93859ec2f1
4 changed files with 85 additions and 44 deletions
+22 -9
View File
@@ -65,7 +65,7 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => {
const { imageUrl: avatarImageUrl } = useAuthorAvatar({ author }); const { imageUrl: avatarImageUrl } = useAuthorAvatar({ author });
const { hideAvatars } = useAvatarVisibilityStore(); const { hideAvatars } = useAvatarVisibilityStore();
const defaultSubplebbits = useDefaultSubplebbits(); const defaultSubplebbits = useDefaultSubplebbits();
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
const params = useParams(); const params = useParams();
const location = useLocation(); const location = useLocation();
@@ -166,7 +166,7 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => {
{isDescription || isRules ? '' : ' '} {isDescription || isRules ? '' : ' '}
</span> </span>
<span className={styles.postNum}> <span className={styles.postNum}>
{subplebbitAddress && (isInAllView || isInSubscriptionsView) && !isReply && ( {subplebbitAddress && (isInAllView || isInSubscriptionsView) && !isReply && boardPath && (
<span className={styles.postNumLink}> <span className={styles.postNumLink}>
{' '} {' '}
<Link to={`/${boardPath}`}>p/{subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress)}</Link>{' '} <Link to={`/${boardPath}`}>p/{subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress)}</Link>{' '}
@@ -175,7 +175,12 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => {
{!(isDescription || isRules) && {!(isDescription || isRules) &&
(cid ? ( (cid ? (
<span className={styles.postNumLink}> <span className={styles.postNumLink}>
<Link to={`/${boardPath}/thread/${cid}`} className={styles.linkToPost} title={t('link_to_post')} onClick={(e) => !cid && e.preventDefault()}> <Link
to={boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`}
className={styles.linkToPost}
title={t('link_to_post')}
onClick={(e) => !cid && e.preventDefault()}
>
c/ c/
</Link> </Link>
<span className={styles.replyToPost} title={t('reply_to_post')} onMouseDown={onReplyModalClick}> <span className={styles.replyToPost} title={t('reply_to_post')} onMouseDown={onReplyModalClick}>
@@ -204,7 +209,13 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => {
<span className={styles.replyButton}> <span className={styles.replyButton}>
[ [
<Link <Link
to={isInAllView && isDescription ? '/all/description' : `/${boardPath}/${isDescription ? 'description' : isRules ? 'rules' : `thread/${postCid}`}`} to={
isInAllView && isDescription
? '/all/description'
: boardPath
? `/${boardPath}/${isDescription ? 'description' : isRules ? 'rules' : `thread/${postCid}`}`
: `/${isDescription ? 'description' : isRules ? 'rules' : `thread/${postCid}`}`
}
onClick={(e) => !cid && !isDescription && !isRules && e.preventDefault()} onClick={(e) => !cid && !isDescription && !isRules && e.preventDefault()}
> >
{_.capitalize(t('reply'))} {_.capitalize(t('reply'))}
@@ -319,9 +330,11 @@ const Reply = ({ postReplyCount, reply, roles }: PostProps) => {
const { author, cid, deleted, link, linkHeight, linkWidth, postCid, reason, removed, spoiler, subplebbitAddress, thumbnailUrl, parentCid } = post || {}; const { author, cid, deleted, link, linkHeight, linkWidth, postCid, reason, removed, spoiler, subplebbitAddress, thumbnailUrl, parentCid } = post || {};
const { isDescription, isRules } = post || {}; // custom properties, not from api const { isDescription, isRules } = post || {}; // custom properties, not from api
const defaultSubplebbits = useDefaultSubplebbits(); const defaultSubplebbits = useDefaultSubplebbits();
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
const isRouteLinkToReply = useLocation().pathname.startsWith(`/${boardPath}/thread/${cid}`); const location = useLocation();
const route = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
const isRouteLinkToReply = cid ? location.pathname.startsWith(route) : false;
const { hidden } = useHide({ cid }); const { hidden } = useHide({ cid });
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
@@ -361,7 +374,7 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true }: PostPr
const isInPendingPostView = isPendingPostView(location.pathname, params); const isInPendingPostView = isPendingPostView(location.pathname, params);
const isInPostPageView = isPostPageView(location.pathname, params); const isInPostPageView = isPostPageView(location.pathname, params);
const defaultSubplebbits = useDefaultSubplebbits(); const defaultSubplebbits = useDefaultSubplebbits();
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
const { hidden, unhide, hide } = useHide({ cid }); const { hidden, unhide, hide } = useHide({ cid });
const isHidden = hidden && !isInPostPageView; const isHidden = hidden && !isInPostPageView;
@@ -438,14 +451,14 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true }: PostPr
<Trans <Trans
i18nKey={'replies_and_links_omitted'} i18nKey={'replies_and_links_omitted'}
shouldUnescape={true} shouldUnescape={true}
components={{ 1: <Link key={cid} to={`/${boardPath}/thread/${cid}`} /> }} components={{ 1: <Link key={cid} to={boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`} /> }}
values={{ repliesCount, linksCount }} values={{ repliesCount, linksCount }}
/> />
) : ( ) : (
<Trans <Trans
i18nKey={'replies_omitted'} i18nKey={'replies_omitted'}
shouldUnescape={true} shouldUnescape={true}
components={{ 1: <Link key={cid} to={`/${boardPath}/thread/${cid}`} /> }} components={{ 1: <Link key={cid} to={boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`} /> }}
values={{ repliesCount }} values={{ repliesCount }}
/> />
)} )}
+20 -7
View File
@@ -51,7 +51,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => {
timestamp, timestamp,
thumbnailUrl, thumbnailUrl,
} = post || {}; } = post || {};
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
const isReply = parentCid; const isReply = parentCid;
const title = post?.title?.trim(); const title = post?.title?.trim();
const { isDescription, isRules } = post || {}; // custom properties, not from api const { isDescription, isRules } = post || {}; // custom properties, not from api
@@ -175,7 +175,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => {
)} )}
</span> </span>
<span className={styles.dateTimePostNum}> <span className={styles.dateTimePostNum}>
{subplebbitAddress && (isInAllView || isInSubscriptionsView) && !isReply && ( {subplebbitAddress && (isInAllView || isInSubscriptionsView) && !isReply && boardPath && (
<div className={styles.postNumLink}> <div className={styles.postNumLink}>
{' '} {' '}
<Link to={`/${boardPath}`}>p/{subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress)}</Link> <Link to={`/${boardPath}`}>p/{subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress)}</Link>
@@ -185,7 +185,12 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => {
{!(isDescription || isRules) && {!(isDescription || isRules) &&
(cid ? ( (cid ? (
<span className={styles.postNumLink}> <span className={styles.postNumLink}>
<Link to={`/${boardPath}/thread/${cid}`} className={styles.linkToPost} title={t('link_to_post')} onClick={(e) => !cid && e.preventDefault()}> <Link
to={boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`}
className={styles.linkToPost}
title={t('link_to_post')}
onClick={(e) => !cid && e.preventDefault()}
>
c/ c/
</Link> </Link>
<span className={styles.replyToPost} title={t('reply_to_post')} onMouseDown={onReplyModalClick}> <span className={styles.replyToPost} title={t('reply_to_post')} onMouseDown={onReplyModalClick}>
@@ -264,8 +269,10 @@ const Reply = ({ postReplyCount, reply, roles }: PostProps) => {
} }
const { author, cid, deleted, postCid, reason, removed, subplebbitAddress } = post || {}; const { author, cid, deleted, postCid, reason, removed, subplebbitAddress } = post || {};
const defaultSubplebbits = useDefaultSubplebbits(); const defaultSubplebbits = useDefaultSubplebbits();
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
const isRouteLinkToReply = useLocation().pathname.startsWith(`/${boardPath}/thread/${cid}`); const location = useLocation();
const route = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
const isRouteLinkToReply = cid ? location.pathname.startsWith(route) : false;
const { hidden } = useHide({ cid }); const { hidden } = useHide({ cid });
return ( return (
@@ -296,7 +303,7 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true }: PostPro
const isInPendingPostView = isPendingPostView(location.pathname, params); const isInPendingPostView = isPendingPostView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params);
const defaultSubplebbits = useDefaultSubplebbits(); const defaultSubplebbits = useDefaultSubplebbits();
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
const linksCount = useCountLinksInReplies(post); const linksCount = useCountLinksInReplies(post);
const replies = useReplies(post); const replies = useReplies(post);
@@ -353,7 +360,13 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true }: PostPro
{linksCount > 0 && ` / ${linksCount} Links`} {linksCount > 0 && ` / ${linksCount} Links`}
</span> </span>
<Link <Link
to={isInAllView && isDescription ? '/all/description' : `/${boardPath}/${isDescription ? 'description' : isRules ? 'rules' : `thread/${cid}`}`} to={
isInAllView && isDescription
? '/all/description'
: boardPath
? `/${boardPath}/${isDescription ? 'description' : isRules ? 'rules' : `thread/${cid}`}`
: `/${isDescription ? 'description' : isRules ? 'rules' : `thread/${cid}`}`
}
className='button' className='button'
> >
{t('view_thread')} {t('view_thread')}
@@ -131,11 +131,14 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
setOutOfViewCid(null); setOutOfViewCid(null);
}; };
const backlinkBoardPath = backlinkReply?.subplebbitAddress ? getBoardPath(backlinkReply.subplebbitAddress, defaultSubplebbits) : undefined;
const backlinkRoute = backlinkReply?.cid ? (backlinkBoardPath ? `/${backlinkBoardPath}/thread/${backlinkReply.cid}` : `/thread/${backlinkReply.cid}`) : '#';
const replyBacklink = ( const replyBacklink = (
<> <>
<Link <Link
className={styles.backlink} className={styles.backlink}
to={`/${getBoardPath(backlinkReply?.subplebbitAddress || '', defaultSubplebbits)}/thread/${backlinkReply?.cid}`} to={backlinkRoute}
ref={refs.setReference} ref={refs.setReference}
onMouseOver={() => handleMouseOver(backlinkReply?.cid)} onMouseOver={() => handleMouseOver(backlinkReply?.cid)}
onMouseLeave={() => handleMouseLeave(backlinkReply?.cid)} onMouseLeave={() => handleMouseLeave(backlinkReply?.cid)}
@@ -159,10 +162,13 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
const { getThreadSigner } = useAnonModeStore(); const { getThreadSigner } = useAnonModeStore();
const threadSigner = quotelinkReply?.postCid ? getThreadSigner(quotelinkReply?.postCid) : null; const threadSigner = quotelinkReply?.postCid ? getThreadSigner(quotelinkReply?.postCid) : null;
const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, defaultSubplebbits) : undefined;
const quotelinkRoute = quotelinkReply?.cid ? (quotelinkBoardPath ? `/${quotelinkBoardPath}/thread/${quotelinkReply.cid}` : `/thread/${quotelinkReply.cid}`) : '#';
const replyQuotelink = ( const replyQuotelink = (
<> <>
<Link <Link
to={`/${getBoardPath(quotelinkReply?.subplebbitAddress || '', defaultSubplebbits)}/thread/${quotelinkReply?.cid}`} to={quotelinkRoute}
ref={refs.setReference} ref={refs.setReference}
className={styles.quoteLink} className={styles.quoteLink}
onMouseOver={() => handleMouseOver(quotelinkReply?.cid)} onMouseOver={() => handleMouseOver(quotelinkReply?.cid)}
@@ -250,16 +256,17 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
> >
{backlinkReply?.shortCid && `>>${backlinkReply?.shortCid}`} {backlinkReply?.shortCid && `>>${backlinkReply?.shortCid}`}
</span> </span>
{backlinkReply?.shortCid && ( {backlinkReply?.shortCid &&
<Link (() => {
to={`/${getBoardPath(backlinkReply?.subplebbitAddress || '', defaultSubplebbits)}/thread/${backlinkReply?.cid}`} const backlinkBoardPath = backlinkReply?.subplebbitAddress ? getBoardPath(backlinkReply.subplebbitAddress, defaultSubplebbits) : undefined;
className={styles.backlinkHash} const backlinkRoute = backlinkReply?.cid ? (backlinkBoardPath ? `/${backlinkBoardPath}/thread/${backlinkReply.cid}` : `/thread/${backlinkReply.cid}`) : '#';
onClick={(e) => handleClick(e, backlinkReply?.cid, backlinkReply?.subplebbitAddress)} return (
> <Link to={backlinkRoute} className={styles.backlinkHash} onClick={(e) => handleClick(e, backlinkReply?.cid, backlinkReply?.subplebbitAddress)}>
{' '} {' '}
# #
</Link> </Link>
)} );
})()}
{hoveredCid === backlinkReply?.cid && {hoveredCid === backlinkReply?.cid &&
outOfViewCid === backlinkReply?.cid && outOfViewCid === backlinkReply?.cid &&
createPortal( createPortal(
@@ -286,16 +293,21 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
{quotelinkReply?.shortCid && `>>${quotelinkReply?.shortCid}`} {quotelinkReply?.shortCid && `>>${quotelinkReply?.shortCid}`}
{(quotelinkReply?.author?.address === account?.author?.address || quotelinkReply?.author?.address === threadSigner?.address) && ' (You)'} {(quotelinkReply?.author?.address === account?.author?.address || quotelinkReply?.author?.address === threadSigner?.address) && ' (You)'}
</span> </span>
{quotelinkReply?.shortCid && ( {quotelinkReply?.shortCid &&
<Link (() => {
className={styles.quoteLink} const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, defaultSubplebbits) : undefined;
to={`/${getBoardPath(quotelinkReply?.subplebbitAddress || '', defaultSubplebbits)}/thread/${quotelinkReply?.cid}`} const quotelinkRoute = quotelinkReply?.cid
onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress)} ? quotelinkBoardPath
> ? `/${quotelinkBoardPath}/thread/${quotelinkReply.cid}`
: `/thread/${quotelinkReply.cid}`
: '#';
return (
<Link className={styles.quoteLink} to={quotelinkRoute} onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress)}>
{' '} {' '}
# #
</Link> </Link>
)} );
})()}
{hoveredCid === quotelinkReply?.cid && {hoveredCid === quotelinkReply?.cid &&
outOfViewCid === quotelinkReply?.cid && outOfViewCid === quotelinkReply?.cid &&
createPortal( createPortal(
+9 -6
View File
@@ -8,9 +8,12 @@ export const extractDirectoryFromTitle = (title: string): string | null => {
return match ? match[1] : null; return match ? match[1] : null;
}; };
// Cache for maps to avoid recreating them on every call // Cache for directory-to-address map
let cachedSubplebbits: MultisubSubplebbit[] | null = null; let cachedSubplebbitsForDirectory: MultisubSubplebbit[] | null = null;
let cachedDirectoryToAddressMap: Map<string, string> | null = null; let cachedDirectoryToAddressMap: Map<string, string> | null = null;
// Cache for address-to-directory map
let cachedSubplebbitsForAddress: MultisubSubplebbit[] | null = null;
let cachedAddressToDirectoryMap: Map<string, string> | null = null; let cachedAddressToDirectoryMap: Map<string, string> | null = null;
/** /**
@@ -19,7 +22,7 @@ let cachedAddressToDirectoryMap: Map<string, string> | null = null;
*/ */
export const getDirectoryToAddressMap = (subplebbits: MultisubSubplebbit[]): Map<string, string> => { export const getDirectoryToAddressMap = (subplebbits: MultisubSubplebbit[]): Map<string, string> => {
// Check if we can use cached map (same array reference) // Check if we can use cached map (same array reference)
if (cachedDirectoryToAddressMap && cachedSubplebbits === subplebbits) { if (cachedDirectoryToAddressMap && cachedSubplebbitsForDirectory === subplebbits) {
return cachedDirectoryToAddressMap; return cachedDirectoryToAddressMap;
} }
@@ -35,7 +38,7 @@ export const getDirectoryToAddressMap = (subplebbits: MultisubSubplebbit[]): Map
// Cache the map and array reference // Cache the map and array reference
cachedDirectoryToAddressMap = map; cachedDirectoryToAddressMap = map;
cachedSubplebbits = subplebbits; cachedSubplebbitsForDirectory = subplebbits;
return map; return map;
}; };
@@ -45,7 +48,7 @@ export const getDirectoryToAddressMap = (subplebbits: MultisubSubplebbit[]): Map
*/ */
export const getAddressToDirectoryMap = (subplebbits: MultisubSubplebbit[]): Map<string, string> => { export const getAddressToDirectoryMap = (subplebbits: MultisubSubplebbit[]): Map<string, string> => {
// Check if we can use cached map (same array reference) // Check if we can use cached map (same array reference)
if (cachedAddressToDirectoryMap && cachedSubplebbits === subplebbits) { if (cachedAddressToDirectoryMap && cachedSubplebbitsForAddress === subplebbits) {
return cachedAddressToDirectoryMap; return cachedAddressToDirectoryMap;
} }
@@ -61,7 +64,7 @@ export const getAddressToDirectoryMap = (subplebbits: MultisubSubplebbit[]): Map
// Cache the map and array reference // Cache the map and array reference
cachedAddressToDirectoryMap = map; cachedAddressToDirectoryMap = map;
cachedSubplebbits = subplebbits; cachedSubplebbitsForAddress = subplebbits;
return map; return map;
}; };