mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
perf(react-doctor): fix compiler-blocking patterns, raise score 72→81
Replace sync setState-in-effect with keyed remount and render-derived patterns in app, error-display, topbar-edit-modal, catalog-filters; use useCurrentTime() instead of Date.now() in hot paths; fix conditional hooks, passive scroll listeners, lodash path imports.
This commit is contained in:
+9
-8
@@ -28,6 +28,7 @@
|
|||||||
"i18next-browser-languagedetector": "7.1.0",
|
"i18next-browser-languagedetector": "7.1.0",
|
||||||
"i18next-http-backend": "2.2.2",
|
"i18next-http-backend": "2.2.2",
|
||||||
"json-stringify-pretty-compact": "4.0.0",
|
"json-stringify-pretty-compact": "4.0.0",
|
||||||
|
"kubo": "0.39.0",
|
||||||
"lodash": "4.17.23",
|
"lodash": "4.17.23",
|
||||||
"memoizee": "0.4.15",
|
"memoizee": "0.4.15",
|
||||||
"react": "19.1.2",
|
"react": "19.1.2",
|
||||||
@@ -43,8 +44,7 @@
|
|||||||
"remark-supersub": "1.0.0",
|
"remark-supersub": "1.0.0",
|
||||||
"tcp-port-used": "1.0.2",
|
"tcp-port-used": "1.0.2",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"zustand": "4.4.3",
|
"zustand": "4.4.3"
|
||||||
"kubo": "0.39.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate:assets": "node scripts/generate-asset-manifest.js",
|
"generate:assets": "node scripts/generate-asset-manifest.js",
|
||||||
@@ -103,7 +103,14 @@
|
|||||||
"@capacitor/android": "7.4.5",
|
"@capacitor/android": "7.4.5",
|
||||||
"@capacitor/cli": "7.4.5",
|
"@capacitor/cli": "7.4.5",
|
||||||
"@capacitor/core": "7.4.5",
|
"@capacitor/core": "7.4.5",
|
||||||
|
"@electron-forge/cli": "7.8.0",
|
||||||
|
"@electron-forge/maker-dmg": "7.8.0",
|
||||||
|
"@electron-forge/maker-squirrel": "7.8.0",
|
||||||
|
"@electron-forge/maker-zip": "7.8.0",
|
||||||
|
"@electron-forge/plugin-auto-unpack-natives": "7.8.0",
|
||||||
"@react-scan/vite-plugin-react-scan": "0.1.8",
|
"@react-scan/vite-plugin-react-scan": "0.1.8",
|
||||||
|
"@reforged/maker-appimage": "5.1.1",
|
||||||
|
"@types/lodash": "4.17.23",
|
||||||
"@types/memoizee": "0.4.9",
|
"@types/memoizee": "0.4.9",
|
||||||
"@typescript/native-preview": "^7.0.0-dev.20251226.1",
|
"@typescript/native-preview": "^7.0.0-dev.20251226.1",
|
||||||
"@vitejs/plugin-react": "4.3.4",
|
"@vitejs/plugin-react": "4.3.4",
|
||||||
@@ -118,12 +125,6 @@
|
|||||||
"cz-conventional-changelog": "3.3.0",
|
"cz-conventional-changelog": "3.3.0",
|
||||||
"decompress": "4.2.1",
|
"decompress": "4.2.1",
|
||||||
"electron": "36.9.5",
|
"electron": "36.9.5",
|
||||||
"@electron-forge/cli": "7.8.0",
|
|
||||||
"@electron-forge/maker-dmg": "7.8.0",
|
|
||||||
"@electron-forge/maker-squirrel": "7.8.0",
|
|
||||||
"@electron-forge/maker-zip": "7.8.0",
|
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "7.8.0",
|
|
||||||
"@reforged/maker-appimage": "5.1.1",
|
|
||||||
"husky": "4.3.8",
|
"husky": "4.3.8",
|
||||||
"isomorphic-fetch": "3.0.0",
|
"isomorphic-fetch": "3.0.0",
|
||||||
"jsdom": "27.3.0",
|
"jsdom": "27.3.0",
|
||||||
|
|||||||
+5
-12
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Navigate, Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
|
import { Navigate, Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
|
||||||
import { useAccount, useAccountComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
import { useAccount, useAccountComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||||
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
|
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
|
||||||
@@ -108,23 +108,16 @@ const BoardLayout = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const GlobalLayout = () => {
|
const GlobalLayout = () => {
|
||||||
const [theme, setTheme] = useState('');
|
|
||||||
const [currentTheme] = useTheme();
|
const [currentTheme] = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentTheme !== theme) {
|
if (currentTheme) {
|
||||||
setTheme(currentTheme);
|
document.body.classList.add(currentTheme);
|
||||||
}
|
|
||||||
}, [currentTheme, theme]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (theme) {
|
|
||||||
document.body.classList.add(theme);
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.classList.remove(theme);
|
document.body.classList.remove(currentTheme);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [theme]);
|
}, [currentTheme]);
|
||||||
|
|
||||||
const { activeCid, parentNumber, threadNumber, threadCid, subplebbitAddress, closeModal, showReplyModal, scrollY } = useReplyModalStore();
|
const { activeCid, parentNumber, threadNumber, threadCid, subplebbitAddress, closeModal, showReplyModal, scrollY } = useReplyModalStore();
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import CatalogSearch from '../catalog-search';
|
|||||||
import Tooltip from '../tooltip';
|
import Tooltip from '../tooltip';
|
||||||
import { ModQueueButton } from '../../views/mod-queue/mod-queue';
|
import { ModQueueButton } from '../../views/mod-queue/mod-queue';
|
||||||
import styles from './board-buttons.module.css';
|
import styles from './board-buttons.module.css';
|
||||||
import { capitalize } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
|
||||||
interface BoardButtonsProps {
|
interface BoardButtonsProps {
|
||||||
address?: string | undefined;
|
address?: string | undefined;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import useIsMobile from '../../hooks/use-is-mobile';
|
|||||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||||
import { shouldShowSnow } from '../../lib/snow';
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
import Tooltip from '../tooltip';
|
import Tooltip from '../tooltip';
|
||||||
import { startCase } from 'lodash';
|
import startCase from 'lodash/startCase';
|
||||||
import { BANNERS } from '../../generated/asset-manifest';
|
import { BANNERS } from '../../generated/asset-manifest';
|
||||||
|
|
||||||
const ImageBanner = () => {
|
const ImageBanner = () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
|||||||
const { filterItems, saveAndApplyFilters, currentSubplebbitAddress } = useCatalogFiltersStore();
|
const { filterItems, saveAndApplyFilters, currentSubplebbitAddress } = useCatalogFiltersStore();
|
||||||
const resetFeed = useFeedResetStore((state) => state.reset);
|
const resetFeed = useFeedResetStore((state) => state.reset);
|
||||||
|
|
||||||
const [localFilterItems, setLocalFilterItems] = useState(
|
const [localFilterItems, setLocalFilterItems] = useState(() =>
|
||||||
filterItems.map((item) => ({
|
filterItems.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
hide: item.hide ?? true,
|
hide: item.hide ?? true,
|
||||||
@@ -22,18 +22,6 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
|||||||
|
|
||||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
|
|
||||||
// Update local items when store changes
|
|
||||||
useEffect(() => {
|
|
||||||
setLocalFilterItems(
|
|
||||||
filterItems.map((item) => ({
|
|
||||||
...item,
|
|
||||||
hide: item.hide ?? true,
|
|
||||||
top: item.top ?? false,
|
|
||||||
color: item.color ?? '',
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}, [filterItems]);
|
|
||||||
|
|
||||||
const handleAddFilter = useCallback(() => {
|
const handleAddFilter = useCallback(() => {
|
||||||
setLocalFilterItems((prev) => {
|
setLocalFilterItems((prev) => {
|
||||||
const newIndex = prev.length;
|
const newIndex = prev.length;
|
||||||
@@ -181,6 +169,7 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
|||||||
const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
|
const currentSubplebbitAddress = useCatalogFiltersStore((state) => state.currentSubplebbitAddress);
|
||||||
const openHelp = () => setShowHelp(true);
|
const openHelp = () => setShowHelp(true);
|
||||||
const closeHelp = () => setShowHelp(false);
|
const closeHelp = () => setShowHelp(false);
|
||||||
|
|
||||||
@@ -207,7 +196,7 @@ const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
|||||||
{!showHelp && <span className={styles.openHelpButton} title={t('help')} onClick={openHelp} />}
|
{!showHelp && <span className={styles.openHelpButton} title={t('help')} onClick={openHelp} />}
|
||||||
<span className={styles.closeButton} title={t('close')} onClick={closeModal} />
|
<span className={styles.closeButton} title={t('close')} onClick={closeModal} />
|
||||||
</div>
|
</div>
|
||||||
{showHelp ? <FiltersProtip /> : <FiltersTable onSave={closeModal} />}
|
{showHelp ? <FiltersProtip /> : <FiltersTable key={currentSubplebbitAddress ?? 'none'} onSave={closeModal} />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import useWindowWidth from '../../hooks/use-window-width';
|
|||||||
import { ContentPreview } from '../../views/home/popular-threads-box';
|
import { ContentPreview } from '../../views/home/popular-threads-box';
|
||||||
import PostMenuDesktop from '../post-desktop/post-menu-desktop';
|
import PostMenuDesktop from '../post-desktop/post-menu-desktop';
|
||||||
import styles from './catalog-row.module.css';
|
import styles from './catalog-row.module.css';
|
||||||
import { capitalize } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||||
|
|
||||||
interface CatalogPostMediaProps {
|
interface CatalogPostMediaProps {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||||||
import styles from './catalog-search.module.css';
|
import styles from './catalog-search.module.css';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
||||||
import { debounce } from 'lodash';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
const CatalogSearch = () => {
|
const CatalogSearch = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import useIsMobile from '../../hooks/use-is-mobile';
|
|||||||
import useChallengesStore from '../../stores/use-challenges-store';
|
import useChallengesStore from '../../stores/use-challenges-store';
|
||||||
import useTheme from '../../hooks/use-theme';
|
import useTheme from '../../hooks/use-theme';
|
||||||
import styles from './challenge-modal.module.css';
|
import styles from './challenge-modal.module.css';
|
||||||
import { capitalize } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
import { useSpring, animated } from '@react-spring/web';
|
import { useSpring, animated } from '@react-spring/web';
|
||||||
import { useDrag } from '@use-gesture/react';
|
import { useDrag } from '@use-gesture/react';
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import ReplyQuotePreview from '../../components/reply-quote-preview';
|
|||||||
import Markdown from '../../components/markdown';
|
import Markdown from '../../components/markdown';
|
||||||
import Tooltip from '../../components/tooltip';
|
import Tooltip from '../../components/tooltip';
|
||||||
import styles from '../../views/post/post.module.css';
|
import styles from '../../views/post/post.module.css';
|
||||||
import { capitalize } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
|
||||||
const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
|
const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
|
||||||
const commentFromStore = useSubplebbitsPagesStore((state) => state.comments[cid]);
|
const commentFromStore = useSubplebbitsPagesStore((state) => state.comments[cid]);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import styles from './edit-menu.module.css';
|
import styles from './edit-menu.module.css';
|
||||||
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
||||||
import useChallengesStore from '../../stores/use-challenges-store';
|
import useChallengesStore from '../../stores/use-challenges-store';
|
||||||
import { capitalize } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import useAuthorPrivileges from '../../hooks/use-author-privileges';
|
import useAuthorPrivileges from '../../hooks/use-author-privileges';
|
||||||
|
|
||||||
|
|||||||
@@ -6,23 +6,17 @@ import styles from './error-display.module.css';
|
|||||||
const ErrorDisplay = ({ error }: { error: any }) => {
|
const ErrorDisplay = ({ error }: { error: any }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [feedbackMessageKey, setFeedbackMessageKey] = useState<string | null>(null);
|
const [feedbackMessageKey, setFeedbackMessageKey] = useState<string | null>(null);
|
||||||
const [shouldShow, setShouldShow] = useState(false);
|
const [showAfterDelay, setShowAfterDelay] = useState(false);
|
||||||
|
|
||||||
|
const hasError = !!(error?.message || error?.stack || error?.details || error);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hasError = !!(error?.message || error?.stack || error?.details || error);
|
if (!hasError) return;
|
||||||
if (!hasError) {
|
const timer = setTimeout(() => setShowAfterDelay(true), 1000);
|
||||||
setShouldShow(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setShouldShow(true);
|
|
||||||
}, 1000); // delay to avoid false positives, for example when accessing cached feeds that may appear offline for a second or so
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [error]);
|
}, [hasError]);
|
||||||
|
|
||||||
if (!shouldShow) {
|
if (!hasError || !showAfterDelay) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
|
|||||||
import useHide from '../../hooks/use-hide';
|
import useHide from '../../hooks/use-hide';
|
||||||
import useStateString from '../../hooks/use-state-string';
|
import useStateString from '../../hooks/use-state-string';
|
||||||
import useScrollToReply from '../../hooks/use-scroll-to-reply';
|
import useScrollToReply from '../../hooks/use-scroll-to-reply';
|
||||||
|
import { useCurrentTime } from '../../hooks/use-current-time';
|
||||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||||
import CommentContent from '../comment-content';
|
import CommentContent from '../comment-content';
|
||||||
import CommentMedia from '../comment-media';
|
import CommentMedia from '../comment-media';
|
||||||
@@ -33,7 +34,8 @@ import ReplyQuotePreview from '../reply-quote-preview';
|
|||||||
import Tooltip from '../tooltip';
|
import Tooltip from '../tooltip';
|
||||||
import { PostProps } from '../../views/post/post';
|
import { PostProps } from '../../views/post/post';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { capitalize, lowerCase } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
import lowerCase from 'lodash/lowerCase';
|
||||||
import { shouldShowSnow } from '../../lib/snow';
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||||
@@ -48,6 +50,13 @@ import { REPLIES_PER_PAGE } from '../../lib/constants';
|
|||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
|
const RepliesFooter = ({ hasMore, loadingString }: { hasMore: boolean; loadingString: string }) =>
|
||||||
|
hasMore ? (
|
||||||
|
<div className={styles.stateString}>
|
||||||
|
<LoadingEllipsis string={loadingString} />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
// Store scroll position for replies virtuoso across navigations
|
// Store scroll position for replies virtuoso across navigations
|
||||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||||
|
|
||||||
@@ -102,6 +111,7 @@ const PostInfo = ({
|
|||||||
const isInPostPageView = isPostPageView(location.pathname, params);
|
const isInPostPageView = isPostPageView(location.pathname, params);
|
||||||
const isInModQueueView = isModQueueView(location.pathname);
|
const isInModQueueView = isModQueueView(location.pathname);
|
||||||
const { getAlertThresholdSeconds } = useModQueueStore();
|
const { getAlertThresholdSeconds } = useModQueueStore();
|
||||||
|
const currentTime = useCurrentTime();
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const accountAddress = account?.author?.address;
|
const accountAddress = account?.author?.address;
|
||||||
|
|
||||||
@@ -199,7 +209,7 @@ const PostInfo = ({
|
|||||||
const alreadyApproved = approved === true;
|
const alreadyApproved = approved === true;
|
||||||
const alreadyRejected = removed === true;
|
const alreadyRejected = removed === true;
|
||||||
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
|
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
|
||||||
const timeWaiting = timestamp ? Date.now() / 1000 - timestamp : 0;
|
const timeWaiting = timestamp ? currentTime - timestamp : 0;
|
||||||
const alertThresholdSeconds = getAlertThresholdSeconds();
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||||
|
|
||||||
@@ -762,7 +772,7 @@ const PostDesktop = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
window.addEventListener('scroll', setLastVirtuosoState);
|
window.addEventListener('scroll', setLastVirtuosoState, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', setLastVirtuosoState);
|
return () => window.removeEventListener('scroll', setLastVirtuosoState);
|
||||||
}, [virtuosoStateKey, showAllReplies, isInPostPageView]);
|
}, [virtuosoStateKey, showAllReplies, isInPostPageView]);
|
||||||
|
|
||||||
@@ -778,13 +788,7 @@ const PostDesktop = ({
|
|||||||
enabled: shouldScrollToReply,
|
enabled: shouldScrollToReply,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Footer component for Virtuoso showing loading state
|
const virtuosoFooter = useCallback(() => <RepliesFooter hasMore={hasMore} loadingString={t('loading')} />, [hasMore, t]);
|
||||||
const RepliesFooter = () =>
|
|
||||||
hasMore ? (
|
|
||||||
<div className={styles.stateString}>
|
|
||||||
<LoadingEllipsis string={t('loading')} />
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.postDesktop}>
|
<div className={styles.postDesktop}>
|
||||||
@@ -885,7 +889,7 @@ const PostDesktop = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
useWindowScroll={true}
|
useWindowScroll={true}
|
||||||
components={{ Footer: RepliesFooter }}
|
components={{ Footer: virtuosoFooter }}
|
||||||
endReached={loadMore}
|
endReached={loadMore}
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
restoreStateFrom={lastVirtuosoState}
|
restoreStateFrom={lastVirtuosoState}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getBoardPath } from '../../../lib/utils/route-utils';
|
|||||||
import { useDirectories } from '../../../hooks/use-directories';
|
import { useDirectories } from '../../../hooks/use-directories';
|
||||||
import { isAllView, isCatalogView, isPostPageView, isSubscriptionsView } from '../../../lib/utils/view-utils';
|
import { isAllView, isCatalogView, isPostPageView, isSubscriptionsView } from '../../../lib/utils/view-utils';
|
||||||
import useHide from '../../../hooks/use-hide';
|
import useHide from '../../../hooks/use-hide';
|
||||||
import { capitalize } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
import { PostMenuProps } from '../../../lib/utils/post-menu-props';
|
import { PostMenuProps } from '../../../lib/utils/post-menu-props';
|
||||||
|
|
||||||
type CopyLinkButtonProps =
|
type CopyLinkButtonProps =
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import usePublishReply from '../../hooks/use-publish-reply';
|
|||||||
import { useFileUpload } from '../../hooks/use-file-upload';
|
import { useFileUpload } from '../../hooks/use-file-upload';
|
||||||
import useMediaHostingStore from '../../stores/use-media-hosting-store';
|
import useMediaHostingStore from '../../stores/use-media-hosting-store';
|
||||||
import styles from './post-form.module.css';
|
import styles from './post-form.module.css';
|
||||||
import { capitalize, debounce } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
// Separate component for offline alert to isolate rerenders from updatingState
|
// Separate component for offline alert to isolate rerenders from updatingState
|
||||||
// Only this component will rerender when updatingState changes, not the whole PostForm
|
// Only this component will rerender when updatingState changes, not the whole PostForm
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
|
|||||||
import useHide from '../../hooks/use-hide';
|
import useHide from '../../hooks/use-hide';
|
||||||
import useStateString from '../../hooks/use-state-string';
|
import useStateString from '../../hooks/use-state-string';
|
||||||
import useScrollToReply from '../../hooks/use-scroll-to-reply';
|
import useScrollToReply from '../../hooks/use-scroll-to-reply';
|
||||||
|
import { useCurrentTime } from '../../hooks/use-current-time';
|
||||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||||
import CommentContent from '../comment-content';
|
import CommentContent from '../comment-content';
|
||||||
import CommentMedia from '../comment-media';
|
import CommentMedia from '../comment-media';
|
||||||
@@ -29,7 +30,8 @@ import PostMenuMobile from './post-menu-mobile';
|
|||||||
import ReplyQuotePreview from '../reply-quote-preview';
|
import ReplyQuotePreview from '../reply-quote-preview';
|
||||||
import Tooltip from '../tooltip';
|
import Tooltip from '../tooltip';
|
||||||
import { PostProps } from '../../views/post/post';
|
import { PostProps } from '../../views/post/post';
|
||||||
import { capitalize, lowerCase } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
import lowerCase from 'lodash/lowerCase';
|
||||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||||
import useChallengesStore from '../../stores/use-challenges-store';
|
import useChallengesStore from '../../stores/use-challenges-store';
|
||||||
@@ -42,6 +44,13 @@ import { REPLIES_PER_PAGE } from '../../lib/constants';
|
|||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
|
const RepliesFooter = ({ hasMore, loadingString }: { hasMore: boolean; loadingString: string }) =>
|
||||||
|
hasMore ? (
|
||||||
|
<div className={styles.stateString}>
|
||||||
|
<LoadingEllipsis string={loadingString} />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
// Store scroll position for replies virtuoso across navigations
|
// Store scroll position for replies virtuoso across navigations
|
||||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||||
|
|
||||||
@@ -66,6 +75,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||||
const isInModQueueView = isModQueueView(location.pathname);
|
const isInModQueueView = isModQueueView(location.pathname);
|
||||||
const { getAlertThresholdSeconds } = useModQueueStore();
|
const { getAlertThresholdSeconds } = useModQueueStore();
|
||||||
|
const currentTime = useCurrentTime();
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const accountAddress = account?.author?.address;
|
const accountAddress = account?.author?.address;
|
||||||
|
|
||||||
@@ -166,7 +176,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
const alreadyApproved = approved === true;
|
const alreadyApproved = approved === true;
|
||||||
const alreadyRejected = removed === true;
|
const alreadyRejected = removed === true;
|
||||||
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
|
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
|
||||||
const timeWaiting = timestamp ? Date.now() / 1000 - timestamp : 0;
|
const timeWaiting = timestamp ? currentTime - timestamp : 0;
|
||||||
const alertThresholdSeconds = getAlertThresholdSeconds();
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||||
|
|
||||||
@@ -562,7 +572,7 @@ const PostMobile = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
window.addEventListener('scroll', setLastVirtuosoState);
|
window.addEventListener('scroll', setLastVirtuosoState, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', setLastVirtuosoState);
|
return () => window.removeEventListener('scroll', setLastVirtuosoState);
|
||||||
}, [virtuosoStateKey, showAllReplies, isInPostPageView]);
|
}, [virtuosoStateKey, showAllReplies, isInPostPageView]);
|
||||||
|
|
||||||
@@ -578,13 +588,7 @@ const PostMobile = ({
|
|||||||
enabled: shouldScrollToReply,
|
enabled: shouldScrollToReply,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Footer component for Virtuoso showing loading state
|
const virtuosoFooter = useCallback(() => <RepliesFooter hasMore={hasMore} loadingString={t('loading')} />, [hasMore, t]);
|
||||||
const RepliesFooter = () =>
|
|
||||||
hasMore ? (
|
|
||||||
<div className={styles.stateString}>
|
|
||||||
<LoadingEllipsis string={t('loading')} />
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -667,7 +671,7 @@ const PostMobile = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
useWindowScroll={true}
|
useWindowScroll={true}
|
||||||
components={{ Footer: RepliesFooter }}
|
components={{ Footer: virtuosoFooter }}
|
||||||
endReached={loadMore}
|
endReached={loadMore}
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
restoreStateFrom={lastVirtuosoState}
|
restoreStateFrom={lastVirtuosoState}
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import useMediaHostingStore from '../../stores/use-media-hosting-store';
|
|||||||
import { useDirectoryByAddress } from '../../hooks/use-directories';
|
import { useDirectoryByAddress } from '../../hooks/use-directories';
|
||||||
import usePublishReply from '../../hooks/use-publish-reply';
|
import usePublishReply from '../../hooks/use-publish-reply';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
|
import { useCurrentTime } from '../../hooks/use-current-time';
|
||||||
import { useFileUpload } from '../../hooks/use-file-upload';
|
import { useFileUpload } from '../../hooks/use-file-upload';
|
||||||
import styles from './reply-modal.module.css';
|
import styles from './reply-modal.module.css';
|
||||||
import { capitalize, debounce } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
import { useSpring, animated } from '@react-spring/web';
|
import { useSpring, animated } from '@react-spring/web';
|
||||||
import { useDrag } from '@use-gesture/react';
|
import { useDrag } from '@use-gesture/react';
|
||||||
|
|
||||||
@@ -146,9 +148,10 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isInAllView = isAllView(location.pathname);
|
const isInAllView = isAllView(location.pathname);
|
||||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||||
|
const currentTime = useCurrentTime();
|
||||||
// Only subscribe to updatedAt to avoid rerenders from updatingState changes
|
// Only subscribe to updatedAt to avoid rerenders from updatingState changes
|
||||||
const updatedAt = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.updatedAt);
|
const updatedAt = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.updatedAt);
|
||||||
const isBoardOffline = updatedAt && updatedAt < Date.now() / 1000 - 60 * 60;
|
const isBoardOffline = updatedAt && updatedAt < currentTime - 60 * 60;
|
||||||
const offlineAlert = updatedAt
|
const offlineAlert = updatedAt
|
||||||
? isBoardOffline && (
|
? isBoardOffline && (
|
||||||
<div className={styles.offlineBoard}>
|
<div className={styles.offlineBoard}>
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
const isAndroid = Capacitor.getPlatform() === 'android';
|
const isAndroid = Capacitor.getPlatform() === 'android';
|
||||||
|
|
||||||
const AccountSettings = () => {
|
// Inner component keyed by account id so state resets when user switches account
|
||||||
|
const AccountSettingsEditor = ({
|
||||||
|
account,
|
||||||
|
}: {
|
||||||
|
account?: { id?: string; name?: string; author?: { address?: string; shortAddress?: string }; [key: string]: unknown };
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const account = useAccount();
|
|
||||||
const [text, setText] = useState('');
|
|
||||||
|
|
||||||
const accountJson = useMemo(
|
const accountJson = useMemo(
|
||||||
() => stringify({ account: { ...account, plebbit: undefined, karma: undefined, plebbitReactOptions: undefined, unreadNotificationCount: undefined } }),
|
() => stringify({ account: { ...account, plebbit: undefined, karma: undefined, plebbitReactOptions: undefined, unreadNotificationCount: undefined } }),
|
||||||
[account],
|
[account],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const [text, setText] = useState(() => accountJson);
|
||||||
setText(accountJson);
|
|
||||||
}, [accountJson]);
|
|
||||||
|
|
||||||
const { accounts } = useAccounts();
|
const { accounts } = useAccounts();
|
||||||
const switchToNewAccountRef = useRef(false);
|
const switchToNewAccountRef = useRef(false);
|
||||||
@@ -92,7 +93,7 @@ const AccountSettings = () => {
|
|||||||
// Create a temporary download link
|
// Create a temporary download link
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = fileUrl;
|
link.href = fileUrl;
|
||||||
link.download = `${account.name}.json`;
|
link.download = `${account?.name ?? 'account'}.json`;
|
||||||
|
|
||||||
// Append the link, trigger the download, then remove the link
|
// Append the link, trigger the download, then remove the link
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
@@ -221,7 +222,7 @@ const AccountSettings = () => {
|
|||||||
<textarea value={text} onChange={(e) => setText(e.target.value)} autoCorrect='off' autoComplete='off' spellCheck='false' />
|
<textarea value={text} onChange={(e) => setText(e.target.value)} autoCorrect='off' autoComplete='off' spellCheck='false' />
|
||||||
<div>
|
<div>
|
||||||
<button onClick={saveAccount}>{t('save_changes')}</button> <button onClick={() => setText(accountJson)}>{t('reset_changes')}</button>
|
<button onClick={saveAccount}>{t('save_changes')}</button> <button onClick={() => setText(accountJson)}>{t('reset_changes')}</button>
|
||||||
<button className={styles.deleteAccount} onClick={() => _deleteAccount(account?.name)}>
|
<button className={styles.deleteAccount} onClick={() => _deleteAccount(account?.name ?? '')}>
|
||||||
{t('delete_account')}
|
{t('delete_account')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,4 +230,9 @@ const AccountSettings = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AccountSettings = () => {
|
||||||
|
const account = useAccount();
|
||||||
|
return <AccountSettingsEditor key={account?.id} account={account} />;
|
||||||
|
};
|
||||||
|
|
||||||
export default AccountSettings;
|
export default AccountSettings;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import styles from './avatar-settings.module.css';
|
|||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import LoadingEllipsis from '../../loading-ellipsis';
|
import LoadingEllipsis from '../../loading-ellipsis';
|
||||||
import ErrorDisplay from '../../error-display/error-display';
|
import ErrorDisplay from '../../error-display/error-display';
|
||||||
import { capitalize } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
|
||||||
const AvatarPreview = ({ avatar }: any) => {
|
const AvatarPreview = ({ avatar }: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
import { Account, setAccount, useAccount } from '@plebbit/plebbit-react-hooks';
|
import { Account, setAccount, useAccount } from '@plebbit/plebbit-react-hooks';
|
||||||
import styles from './crypto-wallets-setting.module.css';
|
import styles from './crypto-wallets-setting.module.css';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { capitalize } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
|
||||||
interface Wallet {
|
interface Wallet {
|
||||||
chainTicker: string;
|
chainTicker: string;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import useAvatarVisibilityStore from '../../../stores/use-avatar-visibility-stor
|
|||||||
import useTheme from '../../../hooks/use-theme';
|
import useTheme from '../../../hooks/use-theme';
|
||||||
import packageJson from '../../../../package.json';
|
import packageJson from '../../../../package.json';
|
||||||
import styles from './interface-settings.module.css';
|
import styles from './interface-settings.module.css';
|
||||||
import { capitalize } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
import useInterfaceSettingsStore from '../../../stores/use-interface-settings-store';
|
import useInterfaceSettingsStore from '../../../stores/use-interface-settings-store';
|
||||||
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
||||||
import useExpandedMediaStore from '../../../stores/use-expanded-media-store';
|
import useExpandedMediaStore from '../../../stores/use-expanded-media-store';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useAccount } from '@plebbit/plebbit-react-hooks';
|
import { useAccount } from '@plebbit/plebbit-react-hooks';
|
||||||
import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store';
|
import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store';
|
||||||
@@ -6,6 +6,92 @@ import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store';
|
|||||||
import { getAllBoardCodes } from '../../constants/board-codes';
|
import { getAllBoardCodes } from '../../constants/board-codes';
|
||||||
import styles from './topbar-edit-modal.module.css';
|
import styles from './topbar-edit-modal.module.css';
|
||||||
|
|
||||||
|
const directoriesToString = (dirs: Set<string>): string => Array.from(dirs).sort().join(' ');
|
||||||
|
|
||||||
|
const stringToDirectories = (str: string): Set<string> => {
|
||||||
|
const codes = str
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((code) => code.length > 0)
|
||||||
|
.map((code) => code.toLowerCase());
|
||||||
|
return new Set(codes);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form component keyed by store values so it remounts with fresh state when modal reopens
|
||||||
|
const TopbarEditModalForm = ({
|
||||||
|
visibleDirectories,
|
||||||
|
showSubscriptionsInTopbar,
|
||||||
|
setDirectoryVisibility,
|
||||||
|
setShowSubscriptionsInTopbar,
|
||||||
|
closeTopbarEditModal,
|
||||||
|
subscriptions,
|
||||||
|
location,
|
||||||
|
}: {
|
||||||
|
visibleDirectories: Set<string>;
|
||||||
|
showSubscriptionsInTopbar: boolean;
|
||||||
|
setDirectoryVisibility: (code: string, visible: boolean) => void;
|
||||||
|
setShowSubscriptionsInTopbar: (show: boolean) => void;
|
||||||
|
closeTopbarEditModal: () => void;
|
||||||
|
subscriptions: string[];
|
||||||
|
location: { pathname: string };
|
||||||
|
}) => {
|
||||||
|
const allBoardCodes = useMemo(() => getAllBoardCodes(), []);
|
||||||
|
const allVisible = allBoardCodes.every((code) => visibleDirectories.has(code));
|
||||||
|
|
||||||
|
const [localDirectoryInput, setLocalDirectoryInput] = useState(() => (allVisible ? '' : directoriesToString(visibleDirectories)));
|
||||||
|
const [showSubscriptions, setShowSubscriptions] = useState(() => showSubscriptionsInTopbar);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (localDirectoryInput.trim() === '') {
|
||||||
|
allBoardCodes.forEach((code) => setDirectoryVisibility(code, true));
|
||||||
|
} else {
|
||||||
|
const inputDirectories = stringToDirectories(localDirectoryInput);
|
||||||
|
allBoardCodes.forEach((code) => setDirectoryVisibility(code, inputDirectories.has(code)));
|
||||||
|
}
|
||||||
|
setShowSubscriptionsInTopbar(showSubscriptions);
|
||||||
|
closeTopbarEditModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className={styles.directoryInput}
|
||||||
|
placeholder='Example: jp tg mu'
|
||||||
|
value={localDirectoryInput}
|
||||||
|
onChange={(e) => setLocalDirectoryInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{subscriptions.length > 0 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.checkboxItem}>
|
||||||
|
<input type='checkbox' id='show-subscriptions' checked={showSubscriptions} onChange={(e) => setShowSubscriptions(e.target.checked)} />
|
||||||
|
<label htmlFor='show-subscriptions'>show subscriptions</label>
|
||||||
|
<span className={styles.editSubscriptionsWrapper}>
|
||||||
|
(
|
||||||
|
<Link
|
||||||
|
to={location.pathname.replace(/\/$/, '') + '/settings#subscriptions-settings'}
|
||||||
|
className={styles.editSubscriptionsLink}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
closeTopbarEditModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
edit subscriptions
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.topbarEditFooter}>
|
||||||
|
<button onClick={handleSave}>Save</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const TopbarEditModal = () => {
|
const TopbarEditModal = () => {
|
||||||
const { showModal, closeTopbarEditModal } = useTopbarEditModalStore();
|
const { showModal, closeTopbarEditModal } = useTopbarEditModalStore();
|
||||||
const { visibleDirectories, showSubscriptionsInTopbar, setDirectoryVisibility, setShowSubscriptionsInTopbar } = useTopbarVisibilityStore();
|
const { visibleDirectories, showSubscriptionsInTopbar, setDirectoryVisibility, setShowSubscriptionsInTopbar } = useTopbarVisibilityStore();
|
||||||
@@ -13,41 +99,6 @@ const TopbarEditModal = () => {
|
|||||||
const subscriptions = useMemo(() => account?.subscriptions || [], [account?.subscriptions]);
|
const subscriptions = useMemo(() => account?.subscriptions || [], [account?.subscriptions]);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// Convert visible directories set to space-separated string for input
|
|
||||||
const directoriesToString = (dirs: Set<string>): string => {
|
|
||||||
return Array.from(dirs).sort().join(' ');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert space-separated string to set of directory codes
|
|
||||||
const stringToDirectories = (str: string): Set<string> => {
|
|
||||||
const codes = str
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter((code) => code.length > 0)
|
|
||||||
.map((code) => code.toLowerCase());
|
|
||||||
return new Set(codes);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Memoize board codes to avoid recalculating
|
|
||||||
const allBoardCodes = useMemo(() => getAllBoardCodes(), []);
|
|
||||||
|
|
||||||
// Check if all directories are visible (default state)
|
|
||||||
const allDirectoriesVisible = useMemo(() => allBoardCodes.every((code) => visibleDirectories.has(code)), [allBoardCodes, visibleDirectories]);
|
|
||||||
|
|
||||||
// Local state for text input (will be saved on Save click)
|
|
||||||
// Empty string means all directories visible (default), otherwise show only the specified codes
|
|
||||||
const [localDirectoryInput, setLocalDirectoryInput] = useState<string>(allDirectoriesVisible ? '' : directoriesToString(visibleDirectories));
|
|
||||||
const [showSubscriptions, setShowSubscriptions] = useState<boolean>(showSubscriptionsInTopbar);
|
|
||||||
|
|
||||||
// Sync local state when modal opens or store changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (showModal) {
|
|
||||||
const allVisible = allBoardCodes.every((code) => visibleDirectories.has(code));
|
|
||||||
setLocalDirectoryInput(allVisible ? '' : directoriesToString(visibleDirectories));
|
|
||||||
setShowSubscriptions(showSubscriptionsInTopbar);
|
|
||||||
}
|
|
||||||
}, [showModal, visibleDirectories, showSubscriptionsInTopbar, allBoardCodes]);
|
|
||||||
|
|
||||||
if (!showModal) {
|
if (!showModal) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -58,25 +109,7 @@ const TopbarEditModal = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const formKey = `${directoriesToString(visibleDirectories)}-${showSubscriptionsInTopbar}`;
|
||||||
// If input is empty, show all directories (default behavior)
|
|
||||||
// Otherwise, show only the directories specified in the input
|
|
||||||
if (localDirectoryInput.trim() === '') {
|
|
||||||
// Show all directories
|
|
||||||
allBoardCodes.forEach((code) => {
|
|
||||||
setDirectoryVisibility(code, true);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Show only specified directories
|
|
||||||
const inputDirectories = stringToDirectories(localDirectoryInput);
|
|
||||||
allBoardCodes.forEach((code) => {
|
|
||||||
setDirectoryVisibility(code, inputDirectories.has(code));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowSubscriptionsInTopbar(showSubscriptions);
|
|
||||||
closeTopbarEditModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.backdrop} onClick={handleBackdropClick}>
|
<div className={styles.backdrop} onClick={handleBackdropClick}>
|
||||||
@@ -86,41 +119,16 @@ const TopbarEditModal = () => {
|
|||||||
<button className={styles.closeButton} onClick={closeTopbarEditModal} title='Close' />
|
<button className={styles.closeButton} onClick={closeTopbarEditModal} title='Close' />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.bd}>
|
<div className={styles.bd}>
|
||||||
<div className={styles.section}>
|
<TopbarEditModalForm
|
||||||
<input
|
key={formKey}
|
||||||
type='text'
|
visibleDirectories={visibleDirectories}
|
||||||
className={styles.directoryInput}
|
showSubscriptionsInTopbar={showSubscriptionsInTopbar}
|
||||||
placeholder='Example: jp tg mu'
|
setDirectoryVisibility={setDirectoryVisibility}
|
||||||
value={localDirectoryInput}
|
setShowSubscriptionsInTopbar={setShowSubscriptionsInTopbar}
|
||||||
onChange={(e) => setLocalDirectoryInput(e.target.value)}
|
closeTopbarEditModal={closeTopbarEditModal}
|
||||||
/>
|
subscriptions={subscriptions}
|
||||||
</div>
|
location={location}
|
||||||
|
/>
|
||||||
{subscriptions.length > 0 && (
|
|
||||||
<div className={styles.section}>
|
|
||||||
<div className={styles.checkboxItem}>
|
|
||||||
<input type='checkbox' id='show-subscriptions' checked={showSubscriptions} onChange={(e) => setShowSubscriptions(e.target.checked)} />
|
|
||||||
<label htmlFor='show-subscriptions'>show subscriptions</label>
|
|
||||||
<span className={styles.editSubscriptionsWrapper}>
|
|
||||||
(
|
|
||||||
<Link
|
|
||||||
to={location.pathname.replace(/\/$/, '') + '/settings#subscriptions-settings'}
|
|
||||||
className={styles.editSubscriptionsLink}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
closeTopbarEditModal();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
edit subscriptions
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={styles.topbarEditFooter}>
|
|
||||||
<button onClick={handleSave}>Save</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store';
|
|||||||
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
||||||
import { BOARD_CODE_GROUPS, getAllBoardCodes } from '../../constants/board-codes';
|
import { BOARD_CODE_GROUPS, getAllBoardCodes } from '../../constants/board-codes';
|
||||||
import styles from './topbar.module.css';
|
import styles from './topbar.module.css';
|
||||||
import { capitalize, debounce, lowerCase } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import lowerCase from 'lodash/lowerCase';
|
||||||
|
|
||||||
const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) => void }) => {
|
const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -322,7 +324,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
|
|||||||
prevScrollPosRef.current = currentScrollPos;
|
prevScrollPosRef.current = currentScrollPos;
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
window.addEventListener('scroll', debouncedHandleScroll);
|
window.addEventListener('scroll', debouncedHandleScroll, { passive: true });
|
||||||
|
|
||||||
return () => window.removeEventListener('scroll', debouncedHandleScroll);
|
return () => window.removeEventListener('scroll', debouncedHandleScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useClientsStates, useSubplebbit, useSubplebbitsStates } from '@plebbit/plebbit-react-hooks';
|
import { useClientsStates, useSubplebbit, useSubplebbitsStates } from '@plebbit/plebbit-react-hooks';
|
||||||
import { debounce } from 'lodash';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
interface CommentOrSubplebbit {
|
interface CommentOrSubplebbit {
|
||||||
state?: string;
|
state?: string;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
|||||||
import DisclaimerModal from '../../components/disclaimer-modal';
|
import DisclaimerModal from '../../components/disclaimer-modal';
|
||||||
import DirectoryModal from '../../components/directory-modal';
|
import DirectoryModal from '../../components/directory-modal';
|
||||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||||
import { lowerCase } from 'lodash';
|
import lowerCase from 'lodash/lowerCase';
|
||||||
|
|
||||||
// https://github.com/bitsocialhq/lists/blob/master/5chan-directories.json
|
// https://github.com/bitsocialhq/lists/blob/master/5chan-directories.json
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const PopularThreadsBox = ({ directories, subplebbits }: { directories: Director
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore();
|
const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore();
|
||||||
|
|
||||||
const getFilteredSubplebbits = () => {
|
const filteredSubplebbits = useMemo(() => {
|
||||||
if (showWorksafeContentOnly) {
|
if (showWorksafeContentOnly) {
|
||||||
return subplebbits.filter((sub: Subplebbit) => {
|
return subplebbits.filter((sub: Subplebbit) => {
|
||||||
const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
|
const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
|
||||||
@@ -84,9 +84,7 @@ const PopularThreadsBox = ({ directories, subplebbits }: { directories: Director
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return subplebbits;
|
return subplebbits;
|
||||||
};
|
}, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, directories]);
|
||||||
|
|
||||||
const filteredSubplebbits = useMemo(getFilteredSubplebbits, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, directories]);
|
|
||||||
const { popularPosts } = usePopularPosts(filteredSubplebbits);
|
const { popularPosts } = usePopularPosts(filteredSubplebbits);
|
||||||
const isLoading = popularPosts.length === 0;
|
const isLoading = popularPosts.length === 0;
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import useChallengesStore from '../../stores/use-challenges-store';
|
|||||||
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
||||||
import Tooltip from '../../components/tooltip';
|
import Tooltip from '../../components/tooltip';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
|
import { useCurrentTime } from '../../hooks/use-current-time';
|
||||||
import { Post } from '../post/post';
|
import { Post } from '../post/post';
|
||||||
import { capitalize, lowerCase } from 'lodash';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
import lowerCase from 'lodash/lowerCase';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
@@ -158,6 +160,7 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getAlertThresholdSeconds } = useModQueueStore();
|
const { getAlertThresholdSeconds } = useModQueueStore();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const currentTime = useCurrentTime();
|
||||||
|
|
||||||
const { editedComment } = useEditedComment({ comment });
|
const { editedComment } = useEditedComment({ comment });
|
||||||
const displayComment = editedComment || comment;
|
const displayComment = editedComment || comment;
|
||||||
@@ -171,7 +174,7 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
|||||||
const alreadyApproved = approved === true;
|
const alreadyApproved = approved === true;
|
||||||
const alreadyRejected = removed === true;
|
const alreadyRejected = removed === true;
|
||||||
|
|
||||||
const timeWaiting = Date.now() / 1000 - timestamp;
|
const timeWaiting = currentTime - timestamp;
|
||||||
const alertThresholdSeconds = getAlertThresholdSeconds();
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
const isOverThreshold = timeWaiting > alertThresholdSeconds;
|
const isOverThreshold = timeWaiting > alertThresholdSeconds;
|
||||||
|
|
||||||
@@ -287,6 +290,7 @@ interface ModQueueCardProps {
|
|||||||
const ModQueueCard = ({ comment }: ModQueueCardProps) => {
|
const ModQueueCard = ({ comment }: ModQueueCardProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getAlertThresholdSeconds } = useModQueueStore();
|
const { getAlertThresholdSeconds } = useModQueueStore();
|
||||||
|
const currentTime = useCurrentTime();
|
||||||
|
|
||||||
const { editedComment } = useEditedComment({ comment });
|
const { editedComment } = useEditedComment({ comment });
|
||||||
const displayComment = editedComment || comment;
|
const displayComment = editedComment || comment;
|
||||||
@@ -297,7 +301,7 @@ const ModQueueCard = ({ comment }: ModQueueCardProps) => {
|
|||||||
const alreadyApproved = approved === true;
|
const alreadyApproved = approved === true;
|
||||||
const alreadyRejected = removed === true;
|
const alreadyRejected = removed === true;
|
||||||
|
|
||||||
const timeWaiting = Date.now() / 1000 - timestamp;
|
const timeWaiting = currentTime - timestamp;
|
||||||
const alertThresholdSeconds = getAlertThresholdSeconds();
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
const isOverThreshold = timeWaiting > alertThresholdSeconds;
|
const isOverThreshold = timeWaiting > alertThresholdSeconds;
|
||||||
const isAwaitingApproval = !alreadyApproved && !alreadyRejected;
|
const isAwaitingApproval = !alreadyApproved && !alreadyRejected;
|
||||||
@@ -441,21 +445,21 @@ const ModQueueBoardFilter = ({ communities }: ModQueueBoardFilterProps) => {
|
|||||||
setSelectedBoardFilter(value);
|
setSelectedBoardFilter(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!communities || communities.length === 0) {
|
// Compute derived values before any early return so hooks run unconditionally
|
||||||
return null;
|
const firstBoardAddress = communities?.find((sub) => sub.address)?.address;
|
||||||
}
|
|
||||||
|
|
||||||
// Default to first board if none selected
|
|
||||||
const firstBoardAddress = communities.find((sub) => sub.address)?.address;
|
|
||||||
const currentFilter = selectedBoardFilter || firstBoardAddress || '';
|
const currentFilter = selectedBoardFilter || firstBoardAddress || '';
|
||||||
|
|
||||||
// Auto-select first board if none is selected
|
// Auto-select first board if none is selected (must run before early return)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedBoardFilter && firstBoardAddress) {
|
if (!selectedBoardFilter && firstBoardAddress) {
|
||||||
setSelectedBoardFilter(firstBoardAddress);
|
setSelectedBoardFilter(firstBoardAddress);
|
||||||
}
|
}
|
||||||
}, [selectedBoardFilter, firstBoardAddress, setSelectedBoardFilter]);
|
}, [selectedBoardFilter, firstBoardAddress, setSelectedBoardFilter]);
|
||||||
|
|
||||||
|
if (!communities || communities.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.filterContainer}>
|
<div className={styles.filterContainer}>
|
||||||
<label>{t('filter_by_board')}:</label>
|
<label>{t('filter_by_board')}:</label>
|
||||||
@@ -488,10 +492,11 @@ interface ModQueueCountItemProps {
|
|||||||
const ModQueueCountItem = ({ comment, alertThresholdSeconds, onStatusChange }: ModQueueCountItemProps) => {
|
const ModQueueCountItem = ({ comment, alertThresholdSeconds, onStatusChange }: ModQueueCountItemProps) => {
|
||||||
const { editedComment } = useEditedComment({ comment });
|
const { editedComment } = useEditedComment({ comment });
|
||||||
const displayComment = editedComment || comment;
|
const displayComment = editedComment || comment;
|
||||||
|
const currentTime = useCurrentTime();
|
||||||
|
|
||||||
const { cid, approved, removed, timestamp } = displayComment;
|
const { cid, approved, removed, timestamp } = displayComment;
|
||||||
const isAwaiting = approved !== true && removed !== true;
|
const isAwaiting = approved !== true && removed !== true;
|
||||||
const timeWaiting = Date.now() / 1000 - timestamp;
|
const timeWaiting = currentTime - timestamp;
|
||||||
const isUrgent = isAwaiting && timeWaiting > alertThresholdSeconds;
|
const isUrgent = isAwaiting && timeWaiting > alertThresholdSeconds;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -5315,6 +5315,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/lodash@4.17.23":
|
||||||
|
version "4.17.23"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.23.tgz#c1bb06db218acc8fc232da0447473fc2fb9d9841"
|
||||||
|
integrity sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==
|
||||||
|
|
||||||
"@types/long@^4.0.1":
|
"@types/long@^4.0.1":
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
|
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
|
||||||
|
|||||||
Reference in New Issue
Block a user