mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
perf(components): prevent rerenders from updatingState
Use Zustand selectors with custom equality functions to only subscribe to specific fields needed by components, avoiding unnecessary rerenders when transient state like updatingState changes. Extract offline indicators into separate components to isolate rerenders. Memoize card components in catalog and popular threads views.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||
import { useAccount, useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
import Plebbit from '@plebbit/plebbit-js';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import Plebbit from '@plebbit/plebbit-js';
|
||||
import { useStableSubplebbit } from '../../hooks/use-stable-subplebbit';
|
||||
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
|
||||
import styles from './board-header.module.css';
|
||||
import { useMultisubMetadata, useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
@@ -21,6 +23,26 @@ const ImageBanner = () => {
|
||||
return <img src={banner} alt='' />;
|
||||
};
|
||||
|
||||
// Separate component for offline indicator to isolate rerenders from updatingState
|
||||
// Only this component will rerender when updatingState changes, not the whole BoardHeader
|
||||
const OfflineIndicator = ({ subplebbitAddress }: { subplebbitAddress: string | undefined }) => {
|
||||
// Subscribe to full subplebbit including transient state for offline detection
|
||||
const subplebbit = useSubplebbitsStore((state) => (subplebbitAddress ? state.subplebbits[subplebbitAddress] : undefined));
|
||||
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit);
|
||||
|
||||
if (!isOffline && !isOnlineStatusLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.offlineIconWrapper}>
|
||||
<Tooltip content={offlineTitle}>
|
||||
<span className={`${styles.offlineIcon} ${offlineIconClass}`} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const BoardHeader = () => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
@@ -33,9 +55,9 @@ const BoardHeader = () => {
|
||||
const resolvedAddress = useResolvedSubplebbitAddress();
|
||||
const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress;
|
||||
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
|
||||
const { address, shortAddress } = subplebbit || {};
|
||||
// Use stable subplebbit for display fields to avoid rerenders from updatingState
|
||||
const stableSubplebbit = useStableSubplebbit(subplebbitAddress);
|
||||
const { address, shortAddress } = stableSubplebbit || {};
|
||||
|
||||
const multisubMetadata = useMultisubMetadata();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
@@ -43,21 +65,23 @@ const BoardHeader = () => {
|
||||
// Find matching subplebbit from default list to get its title
|
||||
const defaultSubplebbit = subplebbitAddress ? defaultSubplebbits.find((s) => s.address === subplebbitAddress) : null;
|
||||
|
||||
const account = useAccount() || {};
|
||||
const subscriptions = account?.subscriptions || [];
|
||||
const subscriptionsSubtitle = t('subscriptions_subtitle', { count: subscriptions?.length || 0 });
|
||||
// Use accounts store with selector to only subscribe to subscriptions count
|
||||
const subscriptionsCount = useAccountsStore((state) => {
|
||||
const activeAccountId = state.activeAccountId;
|
||||
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
|
||||
return activeAccount?.subscriptions?.length || 0;
|
||||
});
|
||||
const subscriptionsSubtitle = t('subscriptions_subtitle', { count: subscriptionsCount });
|
||||
|
||||
const title = isInAllView
|
||||
? multisubMetadata?.title || '/all/ - 5chan Directories'
|
||||
: isInSubscriptionsView
|
||||
? '/subs/ - Subscriptions'
|
||||
: isInModView
|
||||
? _.startCase(t('boards_you_moderate'))
|
||||
: defaultSubplebbit?.title || subplebbit?.title;
|
||||
? '/subs/ - Subscriptions'
|
||||
: isInModView
|
||||
? _.startCase(t('boards_you_moderate'))
|
||||
: defaultSubplebbit?.title || stableSubplebbit?.title;
|
||||
const subtitle = isInAllView ? '' : isInSubscriptionsView ? subscriptionsSubtitle : isInModView ? '/mod/' : `${address || subplebbitAddress || ''}`;
|
||||
|
||||
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit);
|
||||
|
||||
return (
|
||||
<div className={`${styles.content} ${shouldShowSnow() ? styles.garland : ''}`}>
|
||||
{!useIsMobile() && (
|
||||
@@ -72,13 +96,7 @@ const BoardHeader = () => {
|
||||
? shortAddress.slice(0, -4)
|
||||
: shortAddress
|
||||
: subplebbitAddress && Plebbit.getShortAddress({ address: subplebbitAddress }))}
|
||||
{(isOffline || isOnlineStatusLoading) && !isInAllView && !isInSubscriptionsView && !isInModView && (
|
||||
<span className={styles.offlineIconWrapper}>
|
||||
<Tooltip content={offlineTitle}>
|
||||
<span className={`${styles.offlineIcon} ${offlineIconClass}`} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
{!isInAllView && !isInSubscriptionsView && !isInModView && <OfflineIndicator subplebbitAddress={subplebbitAddress} />}
|
||||
</div>
|
||||
<div className={styles.boardSubtitle}>
|
||||
{isInSubscriptionsView ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
@@ -109,202 +109,206 @@ export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight
|
||||
);
|
||||
};
|
||||
|
||||
const CatalogPost = ({ post }: { post: Comment }) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, spoiler, subplebbitAddress, timestamp, title, thumbnailUrl } = post || {};
|
||||
const linkCount = useCountLinksInReplies(post);
|
||||
// Memoize CatalogPost to prevent rerenders when parent rerenders due to updatingState
|
||||
const CatalogPost = memo(
|
||||
({ post }: { post: Comment }) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, spoiler, subplebbitAddress, timestamp, title, thumbnailUrl } = post || {};
|
||||
const linkCount = useCountLinksInReplies(post);
|
||||
|
||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
const { hidden } = useHide({ cid });
|
||||
const { hidden } = useHide({ cid });
|
||||
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : '';
|
||||
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : '';
|
||||
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
||||
|
||||
const postLink = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
|
||||
const postLink = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
|
||||
|
||||
const threadIcons = (
|
||||
<div className={styles.threadIcons}>
|
||||
{pinned && <span className={styles.stickyIcon} title={t('sticky')} />}
|
||||
{locked && <span className={styles.closedIcon} title={t('closed')} />}
|
||||
</div>
|
||||
);
|
||||
const threadIcons = (
|
||||
<div className={styles.threadIcons}>
|
||||
{pinned && <span className={styles.stickyIcon} title={t('sticky')} />}
|
||||
{locked && <span className={styles.closedIcon} title={t('closed')} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||
const [showPortal, setShowPortal] = useState<boolean>(false);
|
||||
const placementRef = useRef<Placement>('right-start');
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||
const [showPortal, setShowPortal] = useState<boolean>(false);
|
||||
const placementRef = useRef<Placement>('right-start');
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const windowWidth = useWindowWidth();
|
||||
const windowWidth = useWindowWidth();
|
||||
|
||||
const { refs, floatingStyles, update } = useFloating({
|
||||
open: showPortal,
|
||||
placement: placementRef.current,
|
||||
middleware: [
|
||||
offset({ mainAxis: 5 }),
|
||||
size({
|
||||
apply({ elements }) {
|
||||
const referenceElement = refs.reference.current;
|
||||
if (referenceElement) {
|
||||
const availableWidthToTheRight = windowWidth - (referenceElement.getBoundingClientRect().left + referenceElement.getBoundingClientRect().width);
|
||||
const availableWidthToTheLeft = referenceElement.getBoundingClientRect().left;
|
||||
const minWidth = windowWidth * 0.25;
|
||||
const { refs, floatingStyles, update } = useFloating({
|
||||
open: showPortal,
|
||||
placement: placementRef.current,
|
||||
middleware: [
|
||||
offset({ mainAxis: 5 }),
|
||||
size({
|
||||
apply({ elements }) {
|
||||
const referenceElement = refs.reference.current;
|
||||
if (referenceElement) {
|
||||
const availableWidthToTheRight = windowWidth - (referenceElement.getBoundingClientRect().left + referenceElement.getBoundingClientRect().width);
|
||||
const availableWidthToTheLeft = referenceElement.getBoundingClientRect().left;
|
||||
const minWidth = windowWidth * 0.25;
|
||||
|
||||
if (availableWidthToTheRight >= minWidth) {
|
||||
placementRef.current = 'right-start';
|
||||
elements.floating.style.maxWidth = `${availableWidthToTheRight - 40}px`;
|
||||
} else if (availableWidthToTheLeft >= minWidth) {
|
||||
placementRef.current = 'left-start';
|
||||
elements.floating.style.maxWidth = `${availableWidthToTheLeft - 25}px`;
|
||||
} else if (availableWidthToTheRight > availableWidthToTheLeft) {
|
||||
placementRef.current = 'right-start';
|
||||
elements.floating.style.maxWidth = `${availableWidthToTheRight - 40}px`;
|
||||
} else {
|
||||
placementRef.current = 'left-start';
|
||||
elements.floating.style.maxWidth = `${availableWidthToTheLeft - 25}px`;
|
||||
if (availableWidthToTheRight >= minWidth) {
|
||||
placementRef.current = 'right-start';
|
||||
elements.floating.style.maxWidth = `${availableWidthToTheRight - 40}px`;
|
||||
} else if (availableWidthToTheLeft >= minWidth) {
|
||||
placementRef.current = 'left-start';
|
||||
elements.floating.style.maxWidth = `${availableWidthToTheLeft - 25}px`;
|
||||
} else if (availableWidthToTheRight > availableWidthToTheLeft) {
|
||||
placementRef.current = 'right-start';
|
||||
elements.floating.style.maxWidth = `${availableWidthToTheRight - 40}px`;
|
||||
} else {
|
||||
placementRef.current = 'left-start';
|
||||
elements.floating.style.maxWidth = `${availableWidthToTheLeft - 25}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update, windowWidth]);
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update, windowWidth]);
|
||||
|
||||
const { replies } = useReplies({ comment: post });
|
||||
const lastReply = replies?.length > 0 ? replies[replies.length - 1] : null;
|
||||
const { replies } = useReplies({ comment: post, flat: true });
|
||||
const lastReply = replies?.length > 0 ? replies[replies.length - 1] : null;
|
||||
|
||||
const { isCommentAuthorMod: isCatalogPostAuthorMod, commentAuthorRole: catalogPostAuthorRole } = useEditCommentPrivileges({
|
||||
commentAuthorAddress: author?.address,
|
||||
subplebbitAddress,
|
||||
});
|
||||
const { isCommentAuthorMod: isLastReplyAuthorMod, commentAuthorRole: lastReplyAuthorRole } = useEditCommentPrivileges({
|
||||
commentAuthorAddress: lastReply?.author?.address,
|
||||
subplebbitAddress,
|
||||
});
|
||||
const { isCommentAuthorMod: isCatalogPostAuthorMod, commentAuthorRole: catalogPostAuthorRole } = useEditCommentPrivileges({
|
||||
commentAuthorAddress: author?.address,
|
||||
subplebbitAddress,
|
||||
});
|
||||
const { isCommentAuthorMod: isLastReplyAuthorMod, commentAuthorRole: lastReplyAuthorRole } = useEditCommentPrivileges({
|
||||
commentAuthorAddress: lastReply?.author?.address,
|
||||
subplebbitAddress,
|
||||
});
|
||||
|
||||
const postContent = (
|
||||
<div className={`${styles.teaser} ${hidden && styles.hidden}`}>
|
||||
{hidden ? (
|
||||
<b>({t('hidden')})</b>
|
||||
) : (
|
||||
<>
|
||||
{title && (
|
||||
<span>
|
||||
<b>{title}</b>
|
||||
{content ? ': ' : ''}
|
||||
</span>
|
||||
)}
|
||||
{content && <ContentPreview content={content} maxLength={9999} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const { imageSize, showOPComment } = useCatalogStyleStore();
|
||||
const maxWidth = imageSize === 'Large' ? '250px' : '150px';
|
||||
const maxHeight = imageSize === 'Large' ? '250px' : '150px';
|
||||
const CSSProperties = {
|
||||
'--maxWidth': maxWidth,
|
||||
'--maxHeight': maxHeight,
|
||||
} as React.CSSProperties;
|
||||
|
||||
const isTextOnlyThread = !hasThumbnail;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${styles.post} ${imageSize === 'Large' ? styles.large : ''}`} style={CSSProperties}>
|
||||
<div onMouseOver={() => setHoveredCid(cid)} onMouseLeave={() => setHoveredCid(null)}>
|
||||
{hidden ? (
|
||||
<Link to={postLink}>
|
||||
<span className={styles.hiddenThumbnail} />
|
||||
</Link>
|
||||
) : hasThumbnail ? (
|
||||
<>
|
||||
{shouldShowSnow() && hasThumbnail && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
||||
<Link to={postLink}>
|
||||
<div
|
||||
className={`${styles.mediaPaddingWrapper} ${hidden && styles.hidden}`}
|
||||
ref={refs.setReference}
|
||||
onMouseOver={() => (timeoutRef.current = setTimeout(() => setShowPortal(true), 250))}
|
||||
onMouseLeave={() => {
|
||||
setShowPortal(false);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{threadIcons}
|
||||
{spoiler ? (
|
||||
<img src='assets/spoiler.png' alt='' />
|
||||
) : (
|
||||
<CatalogPostMedia cid={cid} commentMediaInfo={commentMediaInfo} linkWidth={linkWidth} linkHeight={linkHeight} />
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
threadIcons
|
||||
)}
|
||||
<div className={styles.meta} title='(R)eplies / (L)ink Replies'>
|
||||
R: <b>{replyCount || '0'}</b>
|
||||
{linkCount > 0 && (
|
||||
const postContent = (
|
||||
<div className={`${styles.teaser} ${hidden && styles.hidden}`}>
|
||||
{hidden ? (
|
||||
<b>({t('hidden')})</b>
|
||||
) : (
|
||||
<>
|
||||
{title && (
|
||||
<span>
|
||||
{' '}
|
||||
/ L: <b>{linkCount}</b>
|
||||
<b>{title}</b>
|
||||
{content ? ': ' : ''}
|
||||
</span>
|
||||
)}
|
||||
<span className={`${styles.postMenu} ${hoveredCid && styles.postMenuVisible}`}>
|
||||
<PostMenuDesktop postMenu={postMenuProps} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.postContent}>{(showOPComment || isTextOnlyThread) && (hasThumbnail ? postContent : <Link to={postLink}>{postContent}</Link>)}</div>
|
||||
</div>
|
||||
{content && <ContentPreview content={content} maxLength={9999} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{hoveredCid === cid &&
|
||||
showPortal &&
|
||||
createPortal(
|
||||
<div className={styles.postPreview} ref={refs.setFloating} style={floatingStyles}>
|
||||
{title ? (
|
||||
);
|
||||
|
||||
const { imageSize, showOPComment } = useCatalogStyleStore();
|
||||
const maxWidth = imageSize === 'Large' ? '250px' : '150px';
|
||||
const maxHeight = imageSize === 'Large' ? '250px' : '150px';
|
||||
const CSSProperties = {
|
||||
'--maxWidth': maxWidth,
|
||||
'--maxHeight': maxHeight,
|
||||
} as React.CSSProperties;
|
||||
|
||||
const isTextOnlyThread = !hasThumbnail;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${styles.post} ${imageSize === 'Large' ? styles.large : ''}`} style={CSSProperties}>
|
||||
<div onMouseOver={() => setHoveredCid(cid)} onMouseLeave={() => setHoveredCid(null)}>
|
||||
{hidden ? (
|
||||
<Link to={postLink}>
|
||||
<span className={styles.hiddenThumbnail} />
|
||||
</Link>
|
||||
) : hasThumbnail ? (
|
||||
<>
|
||||
<span className={styles.postSubject}>{title} </span>
|
||||
{t('by')}
|
||||
{shouldShowSnow() && hasThumbnail && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
||||
<Link to={postLink}>
|
||||
<div
|
||||
className={`${styles.mediaPaddingWrapper} ${hidden && styles.hidden}`}
|
||||
ref={refs.setReference}
|
||||
onMouseOver={() => (timeoutRef.current = setTimeout(() => setShowPortal(true), 250))}
|
||||
onMouseLeave={() => {
|
||||
setShowPortal(false);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{threadIcons}
|
||||
{spoiler ? (
|
||||
<img src='assets/spoiler.png' alt='' />
|
||||
) : (
|
||||
<CatalogPostMedia cid={cid} commentMediaInfo={commentMediaInfo} linkWidth={linkWidth} linkHeight={linkHeight} />
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
t('posted_by')
|
||||
)}{' '}
|
||||
<span className={`${styles.postAuthor} ${isCatalogPostAuthorMod && styles.capcode}`}>
|
||||
{author?.displayName || _.capitalize(t('anonymous'))}
|
||||
{isCatalogPostAuthorMod && <span className='capitalize'>{` ## Board ${catalogPostAuthorRole}`}</span>}
|
||||
</span>
|
||||
{(isInAllView || isInSubscriptionsView) && subplebbitAddress && ` to p/${Plebbit.getShortAddress({ address: subplebbitAddress })}`}
|
||||
<span className={styles.postAgo}> {getFormattedTimeAgo(timestamp)}</span>
|
||||
{replyCount > 0 && (
|
||||
<div className={styles.postLast}>
|
||||
{t('last_reply_by')}{' '}
|
||||
<span className={`${styles.postAuthor} ${isLastReplyAuthorMod && styles.capcode}`}>
|
||||
{lastReply?.author?.displayName || _.capitalize(t('anonymous'))}
|
||||
{isLastReplyAuthorMod && ` ## Board ${lastReplyAuthorRole}`}
|
||||
</span>
|
||||
<span className={styles.postAgo}> {getFormattedTimeAgo(lastReply?.timestamp)}</span>
|
||||
</div>
|
||||
threadIcons
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
<div className={styles.meta} title='(R)eplies / (L)ink Replies'>
|
||||
R: <b>{replyCount || '0'}</b>
|
||||
{linkCount > 0 && (
|
||||
<span>
|
||||
{' '}
|
||||
/ L: <b>{linkCount}</b>
|
||||
</span>
|
||||
)}
|
||||
<span className={`${styles.postMenu} ${hoveredCid && styles.postMenuVisible}`}>
|
||||
<PostMenuDesktop postMenu={postMenuProps} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.postContent}>{(showOPComment || isTextOnlyThread) && (hasThumbnail ? postContent : <Link to={postLink}>{postContent}</Link>)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{hoveredCid === cid &&
|
||||
showPortal &&
|
||||
createPortal(
|
||||
<div className={styles.postPreview} ref={refs.setFloating} style={floatingStyles}>
|
||||
{title ? (
|
||||
<>
|
||||
<span className={styles.postSubject}>{title} </span>
|
||||
{t('by')}
|
||||
</>
|
||||
) : (
|
||||
t('posted_by')
|
||||
)}{' '}
|
||||
<span className={`${styles.postAuthor} ${isCatalogPostAuthorMod && styles.capcode}`}>
|
||||
{author?.displayName || _.capitalize(t('anonymous'))}
|
||||
{isCatalogPostAuthorMod && <span className='capitalize'>{` ## Board ${catalogPostAuthorRole}`}</span>}
|
||||
</span>
|
||||
{(isInAllView || isInSubscriptionsView) && subplebbitAddress && ` to p/${Plebbit.getShortAddress({ address: subplebbitAddress })}`}
|
||||
<span className={styles.postAgo}> {getFormattedTimeAgo(timestamp)}</span>
|
||||
{replyCount > 0 && (
|
||||
<div className={styles.postLast}>
|
||||
{t('last_reply_by')}{' '}
|
||||
<span className={`${styles.postAuthor} ${isLastReplyAuthorMod && styles.capcode}`}>
|
||||
{lastReply?.author?.displayName || _.capitalize(t('anonymous'))}
|
||||
{isLastReplyAuthorMod && ` ## Board ${lastReplyAuthorRole}`}
|
||||
</span>
|
||||
<span className={styles.postAgo}> {getFormattedTimeAgo(lastReply?.timestamp)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => prevProps.post?.cid === nextProps.post?.cid,
|
||||
);
|
||||
|
||||
interface CatalogRowProps {
|
||||
index?: number;
|
||||
|
||||
@@ -391,7 +391,7 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true }: PostPr
|
||||
const { hidden, unhide, hide } = useHide({ cid });
|
||||
const isHidden = hidden && !isInPostPageView;
|
||||
|
||||
const { replies, hasMore, loadMore } = useReplies({ comment: post });
|
||||
const { replies, hasMore, loadMore } = useReplies({ comment: post, flat: true });
|
||||
const visiblelinksCount = useCountLinksInReplies(post, 5);
|
||||
const totalLinksCount = useCountLinksInReplies(post);
|
||||
const replyCount = replies?.length;
|
||||
|
||||
@@ -14,6 +14,19 @@ import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbi
|
||||
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
|
||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||
import usePublishPost from '../../hooks/use-publish-post';
|
||||
|
||||
// Separate component for offline alert to isolate rerenders from updatingState
|
||||
// Only this component will rerender when updatingState changes, not the whole PostForm
|
||||
const OfflineAlert = ({ subplebbitAddress }: { subplebbitAddress: string | undefined }) => {
|
||||
const subplebbit = useSubplebbitsStore((state) => (subplebbitAddress ? state.subplebbits[subplebbitAddress] : undefined));
|
||||
const { isOffline, isOnlineStatusLoading, offlineTitle } = useIsSubplebbitOffline(subplebbit);
|
||||
|
||||
if (!isOffline && !isOnlineStatusLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={styles.offlineBoard}>{offlineTitle}</div>;
|
||||
};
|
||||
import usePublishReply from '../../hooks/use-publish-reply';
|
||||
import FileUploader from '../../plugins/file-uploader';
|
||||
import styles from './post-form.module.css';
|
||||
@@ -380,15 +393,11 @@ const PostForm = () => {
|
||||
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
|
||||
const resolvedAddress = useResolvedSubplebbitAddress();
|
||||
const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress;
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
const { isOffline, isOnlineStatusLoading, offlineTitle } = useIsSubplebbitOffline(subplebbit);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.postFormDesktop}>
|
||||
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && (isOffline || isOnlineStatusLoading) && (
|
||||
<div className={styles.offlineBoard}>{offlineTitle}</div>
|
||||
)}
|
||||
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
|
||||
{isThreadClosed ? (
|
||||
<div className={styles.closed}>
|
||||
{t('thread_closed')}
|
||||
@@ -408,9 +417,7 @@ const PostForm = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.postFormMobile}>
|
||||
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && (isOffline || isOnlineStatusLoading) && (
|
||||
<div className={styles.offlineBoard}>{offlineTitle}</div>
|
||||
)}
|
||||
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
|
||||
{isThreadClosed ? (
|
||||
<div className={styles.closed}>
|
||||
{t('thread_closed')}
|
||||
|
||||
@@ -231,7 +231,7 @@ const PostMediaContent = ({ post, link }: { post: any; link: string }) => {
|
||||
|
||||
const ReplyBacklinks = ({ post }: PostProps) => {
|
||||
const { cid, parentCid } = post || {};
|
||||
const { replies } = useReplies({ comment: post });
|
||||
const { replies } = useReplies({ comment: post, flat: true });
|
||||
|
||||
return (
|
||||
cid &&
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { setAccount, useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||
import { formatMarkdown } from '../../lib/utils/post-utils';
|
||||
import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
import { isValidURL } from '../../lib/utils/url-utils';
|
||||
@@ -140,9 +140,9 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const location = useLocation();
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
const { updatedAt } = subplebbit || {};
|
||||
const isBoardOffline = subplebbit?.updatedAt && subplebbit.updatedAt < Date.now() / 1000 - 60 * 60;
|
||||
// Only subscribe to updatedAt to avoid rerenders from updatingState changes
|
||||
const updatedAt = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.updatedAt);
|
||||
const isBoardOffline = updatedAt && updatedAt < Date.now() / 1000 - 60 * 60;
|
||||
const offlineAlert = updatedAt
|
||||
? isBoardOffline && (
|
||||
<div className={styles.offlineBoard}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useAccountComment, useSubplebbitStats } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
|
||||
import useSubplebbitStatsVisibilityStore from '../../stores/use-subplebbit-stats-visibility-store';
|
||||
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
@@ -15,8 +15,9 @@ const SubplebbitStats = () => {
|
||||
const resolvedAddress = useResolvedSubplebbitAddress();
|
||||
const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress;
|
||||
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
const { address, createdAt } = subplebbit || {};
|
||||
// Only subscribe to address and createdAt to avoid rerenders from updatingState changes
|
||||
const address = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.address);
|
||||
const createdAt = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.createdAt);
|
||||
|
||||
let stats = useSubplebbitStats({ subplebbitAddress: address });
|
||||
const { hiddenStats, toggleVisibility } = useSubplebbitStatsVisibilityStore();
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Plebbit from '@plebbit/plebbit-js';
|
||||
import { useAccount, useAccountComment, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
|
||||
import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits';
|
||||
import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
@@ -87,7 +88,6 @@ const findBoardAddressByCode = (code: string, defaultSubplebbits: MultisubSubple
|
||||
|
||||
const TopBarDesktop = () => {
|
||||
const { t } = useTranslation();
|
||||
const account = useAccount();
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const isInCatalogView = isCatalogView(location.pathname, params);
|
||||
@@ -102,9 +102,34 @@ const TopBarDesktop = () => {
|
||||
// Memoize allBoardCodes since it's derived from a constant
|
||||
const allBoardCodes = useMemo(() => getAllBoardCodes(), []);
|
||||
|
||||
const subscriptions = account?.subscriptions || [];
|
||||
const { accountSubplebbits } = useAccountSubplebbits();
|
||||
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
|
||||
// Use accounts store with selective subscriptions to avoid rerenders from updatingState
|
||||
// Only subscribe to subscriptions array and account subplebbit addresses
|
||||
const subscriptions = useAccountsStore(
|
||||
(state) => {
|
||||
const activeAccountId = state.activeAccountId;
|
||||
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
|
||||
return activeAccount?.subscriptions || [];
|
||||
},
|
||||
(prev, next) => {
|
||||
// Shallow compare arrays - only rerender if subscriptions actually change
|
||||
if (prev.length !== next.length) return false;
|
||||
return prev.every((val, idx) => val === next[idx]);
|
||||
},
|
||||
);
|
||||
|
||||
const accountSubplebbitAddresses = useAccountsStore(
|
||||
(state) => {
|
||||
const activeAccountId = state.activeAccountId;
|
||||
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
|
||||
const accountSubplebbits = activeAccount?.subplebbits || {};
|
||||
return Object.keys(accountSubplebbits);
|
||||
},
|
||||
(prev, next) => {
|
||||
// Shallow compare arrays - only rerender if addresses actually change
|
||||
if (prev.length !== next.length) return false;
|
||||
return prev.every((val, idx) => val === next[idx]);
|
||||
},
|
||||
);
|
||||
|
||||
// Filter subscriptions to only show visible ones
|
||||
const visibleSubscriptionAddresses = subscriptions.filter((address: string) => visibleSubscriptions.has(address));
|
||||
@@ -243,8 +268,21 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
|
||||
const boardPath = useBoardPath(subplebbitAddress);
|
||||
const selectValue = isInAllView ? 'all' : isInSubscriptionsView ? 'subs' : boardPath || subplebbitAddress;
|
||||
|
||||
const { accountSubplebbits } = useAccountSubplebbits();
|
||||
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
|
||||
// Use accounts store with selective subscriptions to avoid rerenders from updatingState
|
||||
// Only subscribe to account subplebbit addresses (keys only)
|
||||
const accountSubplebbitAddresses = useAccountsStore(
|
||||
(state) => {
|
||||
const activeAccountId = state.activeAccountId;
|
||||
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
|
||||
const accountSubplebbits = activeAccount?.subplebbits || {};
|
||||
return Object.keys(accountSubplebbits);
|
||||
},
|
||||
(prev, next) => {
|
||||
// Shallow compare arrays - only rerender if addresses actually change
|
||||
if (prev.length !== next.length) return false;
|
||||
return prev.every((val, idx) => val === next[idx]);
|
||||
},
|
||||
);
|
||||
|
||||
// Check if current subplebbit is a directory board
|
||||
const currentIsDirectoryBoard = directoryBoards.some((board) => board.address === subplebbitAddress);
|
||||
|
||||
Reference in New Issue
Block a user