From d78fed1fda3ef1f940e5f4c1d6e9ca119ce8ed81 Mon Sep 17 00:00:00 2001 From: plebeius Date: Fri, 7 Nov 2025 12:50:58 +0100 Subject: [PATCH] refactor(routing): migrate to directory-based board routing --- src/app.tsx | 59 +++++++------ .../board-buttons/board-buttons.tsx | 75 +++++++++------- src/components/board-header/board-header.tsx | 6 +- src/components/catalog-row/catalog-row.tsx | 6 +- src/components/markdown/markdown.tsx | 14 ++- src/components/post-desktop/post-desktop.tsx | 20 +++-- src/components/post-form/post-form.tsx | 9 +- src/components/post-mobile/post-mobile.tsx | 16 +++- .../reply-quote-preview.tsx | 18 ++-- .../subplebbit-stats/subplebbit-stats.tsx | 4 +- src/components/topbar/topbar.tsx | 56 ++++++++---- src/hooks/use-theme.ts | 4 +- src/lib/utils/url-utils.ts | 49 +++++++++-- src/lib/utils/view-utils.ts | 86 ++++++++++++------- src/stores/use-disclaimer-modal-store.ts | 19 ++-- src/views/board/board.tsx | 8 +- src/views/catalog/catalog.tsx | 8 +- src/views/home/boards-list/boards-list.tsx | 10 ++- .../popular-threads-box.tsx | 7 +- src/views/not-found/not-found.tsx | 7 +- src/views/pending-post/pending-post.tsx | 8 +- src/views/post/post.tsx | 6 +- 22 files changed, 332 insertions(+), 163 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index 5225d275..2bcfee62 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -8,6 +8,8 @@ import useCreateBoardModalStore from './stores/use-create-board-modal-store'; import useSpecialThemeStore from './stores/use-special-theme-store'; import useIsMobile from './hooks/use-is-mobile'; import useTheme from './hooks/use-theme'; +import { useDefaultSubplebbits } from './hooks/use-default-subplebbits'; +import { getSubplebbitAddress } from './lib/utils/route-utils'; import styles from './app.module.css'; import Board from './views/board'; import Catalog from './views/catalog'; @@ -27,12 +29,14 @@ import TopBar from './components/topbar'; import SettingsModal from './components/settings-modal'; const BoardLayout = () => { - const { accountCommentIndex, subplebbitAddress } = useParams(); + const { accountCommentIndex, boardIdentifier } = useParams(); const location = useLocation(); const isMobile = useIsMobile(); const isInAllView = isAllView(location.pathname); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInModView = isModView(location.pathname); + const defaultSubplebbits = useDefaultSubplebbits(); + const subplebbitAddress = boardIdentifier ? getSubplebbitAddress(boardIdentifier, defaultSubplebbits) : undefined; const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined }); const { closeCreateBoardModal } = useCreateBoardModalStore(); @@ -130,42 +134,41 @@ const App = () => ( }> } /> - } /> } /> - } /> }> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> - } /> - } /> - } /> - } /> + } /> + } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> + } /> diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx index 0e482dc7..2a5e3da8 100644 --- a/src/components/board-buttons/board-buttons.tsx +++ b/src/components/board-buttons/board-buttons.tsx @@ -4,6 +4,9 @@ import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import { isAllView, isCatalogView, isDescriptionView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; +import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; +import { getBoardPath } from '../../lib/utils/route-utils'; +import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useFeedResetStore from '../../stores/use-feed-reset-store'; @@ -29,19 +32,21 @@ interface BoardButtonsProps { const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => { const { t } = useTranslation(); const params = useParams(); + const defaultSubplebbits = useDefaultSubplebbits(); const createCatalogLink = () => { if (isInAllView) { - if (params?.timeFilterName) return `/p/all/catalog/${params.timeFilterName}`; - return `/p/all/catalog`; + if (params?.timeFilterName) return `/all/catalog/${params.timeFilterName}`; + return `/all/catalog`; } else if (isInSubscriptionsView) { - if (params?.timeFilterName) return `/p/subscriptions/catalog/${params.timeFilterName}`; - return `/p/subscriptions/catalog`; + if (params?.timeFilterName) return `/subscriptions/catalog/${params.timeFilterName}`; + return `/subscriptions/catalog`; } else if (isInModView) { - if (params?.timeFilterName) return `/p/mod/catalog/${params.timeFilterName}`; - return `/p/mod/catalog`; + if (params?.timeFilterName) return `/mod/catalog/${params.timeFilterName}`; + return `/mod/catalog`; } - return `/p/${address}/catalog`; + const boardPath = address ? getBoardPath(address, defaultSubplebbits) : address; + return `/${boardPath}/catalog`; }; return ( @@ -65,19 +70,21 @@ const SubscribeButton = ({ address }: BoardButtonsProps) => { const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => { const { t } = useTranslation(); const params = useParams(); + const defaultSubplebbits = useDefaultSubplebbits(); const createReturnLink = () => { if (isInAllView) { - if (params?.timeFilterName) return `/p/all/${params.timeFilterName}`; - return `/p/all`; + if (params?.timeFilterName) return `/all/${params.timeFilterName}`; + return `/all`; } else if (isInSubscriptionsView) { - if (params?.timeFilterName) return `/p/subscriptions/${params.timeFilterName}`; - return `/p/subscriptions`; + if (params?.timeFilterName) return `/subscriptions/${params.timeFilterName}`; + return `/subscriptions`; } else if (isInModView) { - if (params?.timeFilterName) return `/p/mod/${params.timeFilterName}`; - return `/p/mod`; + if (params?.timeFilterName) return `/mod/${params.timeFilterName}`; + return `/mod`; } - return `/p/${address}`; + const boardPath = address ? getBoardPath(address, defaultSubplebbits) : address; + return `/${boardPath}`; }; return ( @@ -206,16 +213,16 @@ export const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView const timeFilterName = event.target.value; const link = isInAllView ? isInCatalogView - ? `/p/all/catalog/${timeFilterName}` - : `/p/all/${timeFilterName}` + ? `/all/catalog/${timeFilterName}` + : `/all/${timeFilterName}` : isInSubscriptionsView ? isInCatalogView - ? `/p/subscriptions/catalog/${timeFilterName}` - : `/p/subscriptions/${timeFilterName}` + ? `/subscriptions/catalog/${timeFilterName}` + : `/subscriptions/${timeFilterName}` : isInModView ? isInCatalogView - ? `/p/mod/catalog/${timeFilterName}` - : `/p/mod/${timeFilterName}` + ? `/mod/catalog/${timeFilterName}` + : `/mod/${timeFilterName}` : null; link && navigate(link); }; @@ -256,7 +263,8 @@ export const MobileBoardButtons = () => { const isInModView = isModView(location.pathname); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); - const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; + const resolvedAddress = useResolvedSubplebbitAddress(); + const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress; const { filteredCount, searchText } = useCatalogFiltersStore(); @@ -319,11 +327,12 @@ const PostPageStats = () => { const params = useParams(); const location = useLocation(); const isInDescriptionView = isDescriptionView(location.pathname, params); + const resolvedAddress = useResolvedSubplebbitAddress(); const comment = useSubplebbitsPagesStore((state) => state.comments[params?.commentCid as string]); - const subplebbit = useSubplebbitsStore((state) => state.subplebbits[params?.subplebbitAddress as string]); + const subplebbit = useSubplebbitsStore((state) => state.subplebbits[resolvedAddress as string]); - const descriptionReplyCount = location?.pathname.startsWith('/p/all/') ? 0 : subplebbit?.rules?.length > 0 ? 1 : 0; + const descriptionReplyCount = location?.pathname.startsWith('/all/') ? 0 : subplebbit?.rules?.length > 0 ? 1 : 0; const { closed, pinned, replyCount } = comment || {}; const linkCount = useCountLinksInReplies(comment); @@ -344,7 +353,8 @@ export const DesktopBoardButtons = () => { const params = useParams(); const location = useLocation(); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); - const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; + const resolvedAddress = useResolvedSubplebbitAddress(); + const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress; const isInCatalogView = isCatalogView(location.pathname, params); const isInAllView = isAllView(location.pathname); const isInPendingPostPage = isPendingPostView(location.pathname, params); @@ -438,6 +448,9 @@ const SearchOPsBar = () => { const isInAllView = isAllView(location.pathname); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInModView = isModView(location.pathname); + const defaultSubplebbits = useDefaultSubplebbits(); + const resolvedAddress = useResolvedSubplebbitAddress(); + const boardPath = resolvedAddress ? getBoardPath(resolvedAddress, defaultSubplebbits) : params?.boardIdentifier || params?.subplebbitAddress; const handleSearch = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { @@ -447,18 +460,18 @@ const SearchOPsBar = () => { if (isInAllView) { catalogUrl = params?.timeFilterName - ? `/p/all/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}` - : `/p/all/catalog?q=${encodeURIComponent(searchQuery)}`; + ? `/all/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}` + : `/all/catalog?q=${encodeURIComponent(searchQuery)}`; } else if (isInSubscriptionsView) { catalogUrl = params?.timeFilterName - ? `/p/subscriptions/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}` - : `/p/subscriptions/catalog?q=${encodeURIComponent(searchQuery)}`; + ? `/subscriptions/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}` + : `/subscriptions/catalog?q=${encodeURIComponent(searchQuery)}`; } else if (isInModView) { catalogUrl = params?.timeFilterName - ? `/p/mod/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}` - : `/p/mod/catalog?q=${encodeURIComponent(searchQuery)}`; + ? `/mod/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}` + : `/mod/catalog?q=${encodeURIComponent(searchQuery)}`; } else { - catalogUrl = `/p/${params?.subplebbitAddress}/catalog?q=${encodeURIComponent(searchQuery)}`; + catalogUrl = `/${boardPath}/catalog?q=${encodeURIComponent(searchQuery)}`; } navigate(catalogUrl); diff --git a/src/components/board-header/board-header.tsx b/src/components/board-header/board-header.tsx index 69113722..23bf3ecd 100644 --- a/src/components/board-header/board-header.tsx +++ b/src/components/board-header/board-header.tsx @@ -7,6 +7,7 @@ import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subple import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils'; import styles from './board-header.module.css'; import { useMultisubMetadata, useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; +import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; import useIsMobile from '../../hooks/use-is-mobile'; import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline'; import { shouldShowSnow } from '../../lib/snow'; @@ -28,7 +29,8 @@ const BoardHeader = () => { const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInModView = isModView(location.pathname); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); - const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; + const resolvedAddress = useResolvedSubplebbitAddress(); + const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress; const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]); @@ -47,7 +49,7 @@ const BoardHeader = () => { : isInModView ? _.startCase(t('boards_you_moderate')) : defaultSubplebbit?.title || subplebbit?.title; - const subtitle = isInAllView ? 'p/all' : isInSubscriptionsView ? 'p/subscriptions' : isInModView ? 'p/mod' : `p/${address}`; + const subtitle = isInAllView ? 'p/all' : isInSubscriptionsView ? 'p/subscriptions' : isInModView ? 'p/mod' : `p/${address || subplebbitAddress || ''}`; const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit); diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx index a381021f..e3569adf 100644 --- a/src/components/catalog-row/catalog-row.tsx +++ b/src/components/catalog-row/catalog-row.tsx @@ -9,6 +9,8 @@ import { shouldShowSnow } from '../../lib/snow'; import { getHasThumbnail } from '../../lib/utils/media-utils'; import { getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; +import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; +import { getBoardPath } from '../../lib/utils/route-utils'; import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useEditCommentPrivileges from '../../hooks/use-author-privileges'; @@ -138,8 +140,10 @@ const CatalogPost = ({ post }: { post: Comment }) => { const params = useParams(); const isInAllView = isAllView(location.pathname); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); + const defaultSubplebbits = useDefaultSubplebbits(); + const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; - const postLink = isInAllView && isDescription ? `/p/all/description` : `/p/${subplebbitAddress}/${isDescription ? 'description' : isRules ? 'rules' : `c/${cid}`}`; + const postLink = isInAllView && isDescription ? '/all/description' : `/${boardPath}/${isDescription ? 'description' : isRules ? 'rules' : `thread/${cid}`}`; const threadIcons = (
diff --git a/src/components/markdown/markdown.tsx b/src/components/markdown/markdown.tsx index 05f98ef6..020151de 100644 --- a/src/components/markdown/markdown.tsx +++ b/src/components/markdown/markdown.tsx @@ -172,9 +172,9 @@ const renderAnchorLink = (children: React.ReactNode, href: string) => { shouldReplaceText = children[0] === href || children[0].trim() === href.trim(); } - // For display purposes, remove leading slash from paths like "/p/something" + // For display purposes, remove leading slash from paths like "/biz" or board identifiers let displayText: React.ReactNode = children; - if (shouldReplaceText && internalPath.startsWith('/p/')) { + if (shouldReplaceText && internalPath.match(/^\/[^/]+$/)) { displayText = internalPath.substring(1); // Remove leading slash } else if (shouldReplaceText) { displayText = internalPath; @@ -188,7 +188,15 @@ const renderAnchorLink = (children: React.ReactNode, href: string) => { } // Handle hash routes and internal patterns (including routes that start with /#/) - if (href.startsWith('#/') || href.startsWith('/#/') || href.startsWith('/p/') || href.match(/^\/p\/[^/]+(\/c\/[^/]+)?$/)) { + // Support both old format (/p/...) for backward compatibility and new format (/{boardIdentifier}/...) + if ( + href.startsWith('#/') || + href.startsWith('/#/') || + href.startsWith('/p/') || + href.match(/^\/p\/[^/]+(\/c\/[^/]+)?$/) || + href.match(/^\/[^/]+(\/thread\/[^/]+)?$/) || + href.match(/^\/[^/]+\/(catalog|description|rules)(\/settings)?$/) + ) { return {children}; } diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index 97661cc5..e0071e3d 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -10,6 +10,8 @@ import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/po import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { isValidURL } from '../../lib/utils/url-utils'; import { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; +import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; +import { getBoardPath } from '../../lib/utils/route-utils'; import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store'; import useAuthorAddressClick from '../../hooks/use-author-address-click'; import { useCommentMediaInfo } from '../../hooks/use-comment-media-info'; @@ -62,6 +64,8 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => { const { showOmittedReplies } = useShowOmittedReplies(); const { imageUrl: avatarImageUrl } = useAuthorAvatar({ author }); const { hideAvatars } = useAvatarVisibilityStore(); + const defaultSubplebbits = useDefaultSubplebbits(); + const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const params = useParams(); const location = useLocation(); @@ -165,13 +169,13 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => { {subplebbitAddress && (isInAllView || isInSubscriptionsView) && !isReply && ( {' '} - p/{subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress)}{' '} + p/{subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress)}{' '} )} {!(isDescription || isRules) && (cid ? ( - !cid && e.preventDefault()}> + !cid && e.preventDefault()}> c/ @@ -200,7 +204,7 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => { [ !cid && !isDescription && !isRules && e.preventDefault()} > {_.capitalize(t('reply'))} @@ -314,8 +318,10 @@ const Reply = ({ postReplyCount, reply, roles }: PostProps) => { const { author, cid, deleted, link, linkHeight, linkWidth, postCid, reason, removed, spoiler, subplebbitAddress, thumbnailUrl, parentCid } = post || {}; const { isDescription, isRules } = post || {}; // custom properties, not from api + const defaultSubplebbits = useDefaultSubplebbits(); + const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; - const isRouteLinkToReply = useLocation().pathname.startsWith(`/p/${subplebbitAddress}/c/${cid}`); + const isRouteLinkToReply = useLocation().pathname.startsWith(`/${boardPath}/thread/${cid}`); const { hidden } = useHide({ cid }); const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); @@ -354,6 +360,8 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true }: PostPr const location = useLocation(); const isInPendingPostView = isPendingPostView(location.pathname, params); const isInPostPageView = isPostPageView(location.pathname, params); + const defaultSubplebbits = useDefaultSubplebbits(); + const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const { hidden, unhide, hide } = useHide({ cid }); const isHidden = hidden && !isInPostPageView; @@ -430,14 +438,14 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true }: PostPr }} + components={{ 1: }} values={{ repliesCount, linksCount }} /> ) : ( }} + components={{ 1: }} values={{ repliesCount }} /> )} diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx index f66352a8..c99790ca 100644 --- a/src/components/post-form/post-form.tsx +++ b/src/components/post-form/post-form.tsx @@ -11,6 +11,7 @@ import { isValidURL } from '../../lib/utils/url-utils'; import { isAllView, isDescriptionView, isModView, isPostPageView, isRulesView, isSubscriptionsView } from '../../lib/utils/view-utils'; import useAnonMode from '../../hooks/use-anon-mode'; import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; +import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame'; import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline'; import usePublishPost from '../../hooks/use-publish-post'; @@ -45,7 +46,8 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: const author = account?.author || {}; const { displayName } = author || {}; const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); - const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; + const resolvedAddress = useResolvedSubplebbitAddress(); + const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress; const { setPublishPostOptions, postIndex, publishPost, publishPostOptions, resetPublishPostOptions } = usePublishPost({ subplebbitAddress }); const textRef = useRef(null); @@ -160,7 +162,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: if (typeof postIndex === 'number') { resetPublishPostOptions(); resetFields(); - navigate(`/profile/${postIndex}`); + navigate(`/pending/${postIndex}`); } }, [postIndex, resetPublishPostOptions, navigate]); @@ -458,7 +460,8 @@ const PostForm = () => { const [showForm, setShowForm] = useState(false); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); - const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; + const resolvedAddress = useResolvedSubplebbitAddress(); + const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress; const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]); const { isOffline, isOnlineStatusLoading, offlineTitle } = useIsSubplebbitOffline(subplebbit); diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 6a9b7db9..2a44f53a 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -10,6 +10,8 @@ import { getHasThumbnail } from '../../lib/utils/media-utils'; import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/post-utils'; import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; +import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; +import { getBoardPath } from '../../lib/utils/route-utils'; import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store'; import useAuthorAddressClick from '../../hooks/use-author-address-click'; import { useCommentMediaInfo } from '../../hooks/use-comment-media-info'; @@ -29,6 +31,7 @@ import useReplyModalStore from '../../stores/use-reply-modal-store'; const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => { const { t } = useTranslation(); + const defaultSubplebbits = useDefaultSubplebbits(); const { author, cid, @@ -48,6 +51,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => { timestamp, thumbnailUrl, } = post || {}; + const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const isReply = parentCid; const title = post?.title?.trim(); const { isDescription, isRules } = post || {}; // custom properties, not from api @@ -174,14 +178,14 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => { {subplebbitAddress && (isInAllView || isInSubscriptionsView) && !isReply && (
{' '} - p/{subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress)} + p/{subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress)}
)} {getFormattedDate(timestamp)}
} content={getFormattedTimeAgo(timestamp)} />{' '} {!(isDescription || isRules) && (cid ? ( - !cid && e.preventDefault()}> + !cid && e.preventDefault()}> c/ @@ -259,7 +263,9 @@ const Reply = ({ postReplyCount, reply, roles }: PostProps) => { post = editedComment; } const { author, cid, deleted, postCid, reason, removed, subplebbitAddress } = post || {}; - const isRouteLinkToReply = useLocation().pathname.startsWith(`/p/${subplebbitAddress}/c/${cid}`); + const defaultSubplebbits = useDefaultSubplebbits(); + const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; + const isRouteLinkToReply = useLocation().pathname.startsWith(`/${boardPath}/thread/${cid}`); const { hidden } = useHide({ cid }); return ( @@ -289,6 +295,8 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true }: PostPro const isInAllView = isAllView(location.pathname); const isInPendingPostView = isPendingPostView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params); + const defaultSubplebbits = useDefaultSubplebbits(); + const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; const linksCount = useCountLinksInReplies(post); const replies = useReplies(post); @@ -345,7 +353,7 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true }: PostPro {linksCount > 0 && ` / ${linksCount} Links`} {t('view_thread')} diff --git a/src/components/reply-quote-preview/reply-quote-preview.tsx b/src/components/reply-quote-preview/reply-quote-preview.tsx index 2d2a71a0..929dd8e8 100644 --- a/src/components/reply-quote-preview/reply-quote-preview.tsx +++ b/src/components/reply-quote-preview/reply-quote-preview.tsx @@ -3,6 +3,8 @@ import { createPortal } from 'react-dom'; import { Link, useNavigate } from 'react-router-dom'; import { Comment, useAccount } from '@plebbit/plebbit-react-hooks'; import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react'; +import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; +import { getBoardPath } from '../../lib/utils/route-utils'; import useAnonModeStore from '../../stores/use-anon-mode-store'; import useIsMobile from '../../hooks/use-is-mobile'; import styles from '../../views/post/post.module.css'; @@ -58,6 +60,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i const [outOfViewCid, setOutOfViewCid] = useState(null); const placementRef = useRef('right'); const availableWidthRef = useRef(0); + const defaultSubplebbits = useDefaultSubplebbits(); const { refs, floatingStyles, update } = useFloating({ placement: placementRef.current, @@ -100,7 +103,8 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined) => { e.preventDefault(); if (cid && subplebbitAddress) { - navigate(`/p/${subplebbitAddress}/c/${cid}`); + const boardPath = getBoardPath(subplebbitAddress, defaultSubplebbits); + navigate(`/${boardPath}/thread/${cid}`); setTimeout(() => { const element = document.querySelector(`[data-cid="${cid}"]`); element?.scrollIntoView(); @@ -131,7 +135,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i <> handleMouseOver(backlinkReply?.cid)} onMouseLeave={() => handleMouseLeave(backlinkReply?.cid)} @@ -158,7 +162,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i const replyQuotelink = ( <> handleMouseOver(quotelinkReply?.cid)} @@ -186,6 +190,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => { const [hoveredCid, setHoveredCid] = useState(null); const [outOfViewCid, setOutOfViewCid] = useState(null); + const defaultSubplebbits = useDefaultSubplebbits(); const { refs, floatingStyles, update } = useFloating({ placement: 'bottom', @@ -207,7 +212,8 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined) => { e.preventDefault(); if (cid && subplebbitAddress) { - navigate(`/p/${subplebbitAddress}/c/${cid}`); + const boardPath = getBoardPath(subplebbitAddress, defaultSubplebbits); + navigate(`/${boardPath}/thread/${cid}`); setTimeout(() => { const element = document.querySelector(`[data-cid="${cid}"]`); element?.scrollIntoView(); @@ -246,7 +252,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is {backlinkReply?.shortCid && ( handleClick(e, backlinkReply?.cid, backlinkReply?.subplebbitAddress)} > @@ -283,7 +289,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is {quotelinkReply?.shortCid && ( handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress)} > {' '} diff --git a/src/components/subplebbit-stats/subplebbit-stats.tsx b/src/components/subplebbit-stats/subplebbit-stats.tsx index de712dff..cbced44a 100644 --- a/src/components/subplebbit-stats/subplebbit-stats.tsx +++ b/src/components/subplebbit-stats/subplebbit-stats.tsx @@ -5,6 +5,7 @@ import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subple import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import useSubplebbitStatsVisibilityStore from '../../stores/use-subplebbit-stats-visibility-store'; import { isDescriptionView, isRulesView } from '../../lib/utils/view-utils'; +import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; import styles from './subplebbit-stats.module.css'; const SubplebbitStats = () => { @@ -12,7 +13,8 @@ const SubplebbitStats = () => { const params = useParams(); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); - const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; + const resolvedAddress = useResolvedSubplebbitAddress(); + const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress; const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]); const { address, createdAt } = subplebbit || {}; diff --git a/src/components/topbar/topbar.tsx b/src/components/topbar/topbar.tsx index b5c0596e..dbd488f7 100644 --- a/src/components/topbar/topbar.tsx +++ b/src/components/topbar/topbar.tsx @@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next'; import Plebbit from '@plebbit/plebbit-js'; import { useAccount, useAccountComment, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks'; import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils'; -import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; +import { useDefaultSubplebbitAddresses, useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; +import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; +import { getBoardPath } from '../../lib/utils/route-utils'; import { TimeFilter } from '../board-buttons'; import useCreateBoardModalStore from '../../stores/use-create-board-modal-store'; import styles from './topbar.module.css'; @@ -55,7 +57,7 @@ const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) => const searchInput = searchInputRef.current?.value; if (searchInput) { searchInputRef.current.value = ''; - navigate(`/p/${searchInput}`); + navigate(`/${searchInput}`); setShowSearchBar(false); } }; @@ -77,6 +79,7 @@ const TopBarDesktop = () => { const isInCatalogView = isCatalogView(location.pathname, params); const [showSearchBar, setShowSearchBar] = useState(false); const { openCreateBoardModal } = useCreateBoardModalStore(); + const defaultSubplebbits = useDefaultSubplebbits(); const subscriptions = account?.subscriptions; @@ -86,26 +89,28 @@ const TopBarDesktop = () => { return (
- [all / subscriptions + [all / subscriptions {accountSubplebbitAddresses.length > 0 && ( <> {' '} - / mod + / mod )} ]{' '} {subscriptions?.length > 0 && ( <> [ - {subscriptions.map((address: any, index: any) => ( - - {index === 0 ? null : ' '} - - {address.endsWith('.eth') || address.endsWith('.sol') ? address : address.slice(0, 10).concat('...')} - - {index !== subscriptions?.length - 1 ? ' /' : null} - - ))} + {subscriptions.map((address: any, index: any) => { + const boardPath = getBoardPath(address, defaultSubplebbits); + const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : address.slice(0, 10).concat('...'); + return ( + + {index === 0 ? null : ' '} + {boardPath && boardPath.trim() ? {displayText} : {displayText}} + {index !== subscriptions?.length - 1 ? ' /' : null} + + ); + })} ]{' '} )} @@ -128,6 +133,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => { const { t } = useTranslation(); const navigate = useNavigate(); const subplebbitAddresses = useDefaultSubplebbitAddresses(); + const defaultSubplebbits = useDefaultSubplebbits(); const displaySubplebbitAddress = subplebbitAddress && subplebbitAddress.length > 30 ? subplebbitAddress.slice(0, 30).concat('...') : subplebbitAddress; const [showSearchBar, setShowSearchBar] = useState(false); @@ -138,21 +144,36 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => { const isInAllView = isAllView(location.pathname); const isInCatalogView = isCatalogView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); - const selectValue = isInAllView ? 'all' : isInSubscriptionsView ? 'subscriptions' : subplebbitAddress; + const boardPath = useBoardPath(subplebbitAddress); + const selectValue = isInAllView ? 'all' : isInSubscriptionsView ? 'subscriptions' : boardPath || subplebbitAddress; const { accountSubplebbits } = useAccountSubplebbits(); const accountSubplebbitAddresses = Object.keys(accountSubplebbits); const boardSelect = ( - { + const value = e.target.value; + // If it's a directory code or special route, use it directly + if (value === 'all' || value === 'subscriptions' || value === 'mod') { + navigate(`/${value}${isInCatalogView ? '/catalog' : ''}`); + } else { + // Otherwise, resolve to board path + const path = getBoardPath(value, defaultSubplebbits); + navigate(`/${path}${isInCatalogView ? '/catalog' : ''}`); + } + }} + > {!currentSubplebbitIsInList && subplebbitAddress && } {accountSubplebbitAddresses.length > 0 && } {subplebbitAddresses.map((address: any, index: number) => { const subplebbitAddress = address?.includes('.') ? address : Plebbit.getShortAddress(address); + const boardPath = getBoardPath(address, defaultSubplebbits); return ( - ); @@ -200,7 +221,8 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => { const TopBar = () => { const params = useParams(); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); - const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; + const resolvedSubplebbitAddress = useResolvedSubplebbitAddress(); + const subplebbitAddress = resolvedSubplebbitAddress || accountComment?.subplebbitAddress; return ( <> diff --git a/src/hooks/use-theme.ts b/src/hooks/use-theme.ts index 2084dab2..01bc4b00 100644 --- a/src/hooks/use-theme.ts +++ b/src/hooks/use-theme.ts @@ -3,6 +3,7 @@ import { useLocation, useParams } from 'react-router-dom'; import { isAllView, isSubscriptionsView, isModView } from '../lib/utils/view-utils'; import useThemeStore from '../stores/use-theme-store'; import { useDefaultSubplebbits } from './use-default-subplebbits'; +import { useResolvedSubplebbitAddress } from './use-resolved-subplebbit-address'; import { useAccountComment } from '@plebbit/plebbit-react-hooks'; import useSpecialThemeStore from '../stores/use-special-theme-store'; import { isChristmas } from '../lib/utils/time-utils'; @@ -33,7 +34,8 @@ const useTheme = (): [string, (theme: string) => void] => { const isInAllView = isAllView(location.pathname); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const isInModView = isModView(location.pathname); - const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress; + const resolvedAddress = useResolvedSubplebbitAddress(); + const subplebbitAddress = resolvedAddress || pendingPostSubplebbitAddress; // Check for Christmas and initialize special theme if needed useEffect(() => { diff --git a/src/lib/utils/url-utils.ts b/src/lib/utils/url-utils.ts index 4fea811a..09a7076b 100644 --- a/src/lib/utils/url-utils.ts +++ b/src/lib/utils/url-utils.ts @@ -42,17 +42,30 @@ export const is5chanLink = (url: string): boolean => { routePath = parsedUrl.hash.substring(1); // Remove the # to get the path } - // For pleb.bz, only support the exact sharelink format + // For pleb.bz, only support the exact sharelink format (legacy /p/... format) if (hostname === 'pleb.bz') { // Must match exactly: /p/{subplebbitAddress}/c/{cid} // Allow redirect parameter since these are still valid internal links return /^\/p\/[^/]+\/c\/[^/]+$/.test(routePath); } - // For other 5chan hostnames, support: + // For other 5chan hostnames, support both old and new formats: + // Old format (for backward compatibility): // - /p/{subplebbitAddress} // - /p/{subplebbitAddress}/c/{commentCid} - return /^\/p\/[^/]+(\/c\/[^/]+)?$/.test(routePath); + // New format: + // - /{boardIdentifier} (directory code or address) + // - /{boardIdentifier}/thread/{commentCid} + // - /{boardIdentifier}/catalog + // - /{boardIdentifier}/description + // - /{boardIdentifier}/rules + // - /all, /subscriptions, /mod, /pending/{index} + return ( + /^\/p\/[^/]+(\/c\/[^/]+)?$/.test(routePath) || + /^\/[^/]+(\/thread\/[^/]+|\/catalog|\/description|\/rules)?$/.test(routePath) || + /^\/(all|subscriptions|mod)(\/catalog|\/thread\/[^/]+)?(\/[^/]+)?$/.test(routePath) || + /^\/pending\/[^/]+$/.test(routePath) + ); } catch { return false; } @@ -71,7 +84,8 @@ export const transform5chanLinkToInternal = (url: string): string | null => { if (parsedUrl.hash && parsedUrl.hash.startsWith('#/')) { // Extract the route from the hash, preserving any query params within the hash const hashPath = parsedUrl.hash.substring(1); // Remove the # - return hashPath; + // Transform old /p/... format to new format if needed + return transformOldPathToNew(hashPath); } // For regular pathname-based routes, remove redirect parameter from query string @@ -81,12 +95,37 @@ export const transform5chanLinkToInternal = (url: string): string | null => { const cleanSearch = searchParams.toString(); const searchString = cleanSearch ? `?${cleanSearch}` : ''; - return parsedUrl.pathname + searchString + parsedUrl.hash; + // Transform old /p/... format to new format if needed + const transformedPath = transformOldPathToNew(parsedUrl.pathname); + return transformedPath + searchString + parsedUrl.hash; } catch { return null; } }; +// Transform old URL format (/p/{address}/c/{cid}) to new format (/{boardIdentifier}/thread/{cid}) +// Note: This function doesn't resolve directory codes - that's handled by the routing system +const transformOldPathToNew = (path: string): string => { + // Transform /p/{address}/c/{cid} to /{address}/thread/{cid} + const oldPostPattern = /^\/p\/([^/]+)\/c\/([^/]+)$/; + const postMatch = path.match(oldPostPattern); + if (postMatch) { + const [, address, cid] = postMatch; + return `/${address}/thread/${cid}`; + } + + // Transform /p/{address} to /{address} + const oldBoardPattern = /^\/p\/([^/]+)$/; + const boardMatch = path.match(oldBoardPattern); + if (boardMatch) { + const [, address] = boardMatch; + return `/${address}`; + } + + // Return path as-is if it doesn't match old patterns + return path; +}; + // Check if a string is a valid IPNS public key (52 chars starting with 12D3KooW) const isValidIPNSKey = (str: string): boolean => { return str.length === 52 && str.startsWith('12D3KooW'); diff --git a/src/lib/utils/view-utils.ts b/src/lib/utils/view-utils.ts index e0371cee..d4d307d9 100644 --- a/src/lib/utils/view-utils.ts +++ b/src/lib/utils/view-utils.ts @@ -1,41 +1,60 @@ export type ParamsType = { accountCommentIndex?: string; + boardIdentifier?: string; commentCid?: string; - subplebbitAddress?: string; + subplebbitAddress?: string; // deprecated, kept for backward compatibility timeFilterName?: string; }; export const isAllView = (pathname: string): boolean => { - return pathname.startsWith('/p/all'); + return pathname.startsWith('/all'); }; export const isBoardView = (pathname: string, params: ParamsType): boolean => { // some subs might use emojis in their address, so we need to decode the pathname const decodedPathname = decodeURIComponent(pathname); - return params.subplebbitAddress ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}`) : false; + // Check if it's a board view (not all, subscriptions, mod, pending, or special routes) + if ( + pathname.startsWith('/all') || + pathname.startsWith('/subscriptions') || + pathname.startsWith('/mod') || + pathname.startsWith('/pending') || + pathname === '/' || + pathname.startsWith('/faq') || + pathname.startsWith('/not-found') + ) { + return false; + } + const identifier = params.boardIdentifier || params.subplebbitAddress; + return identifier ? decodedPathname.startsWith(`/${identifier}`) : false; }; export const isCatalogView = (pathname: string, params: ParamsType): boolean => { - const { subplebbitAddress, timeFilterName } = params; + const { boardIdentifier, subplebbitAddress, timeFilterName } = params; + const identifier = boardIdentifier || subplebbitAddress; const decodedPathname = decodeURIComponent(pathname); return ( - decodedPathname === `/p/${subplebbitAddress}/catalog` || - decodedPathname === `/p/${subplebbitAddress}/catalog/settings` || - decodedPathname === `/p/all/catalog` || - decodedPathname === `/p/all/catalog/settings` || - decodedPathname === `/p/all/catalog/${timeFilterName}` || - decodedPathname === `/p/all/catalog/${timeFilterName}/settings` || - decodedPathname === `/p/subscriptions/catalog` || - decodedPathname === `/p/subscriptions/catalog/settings` || - decodedPathname === `/p/subscriptions/catalog/${timeFilterName}` || - decodedPathname === `/p/subscriptions/catalog/${timeFilterName}/settings` + (identifier && (decodedPathname === `/${identifier}/catalog` || decodedPathname === `/${identifier}/catalog/settings`)) || + decodedPathname === `/all/catalog` || + decodedPathname === `/all/catalog/settings` || + decodedPathname === `/all/catalog/${timeFilterName}` || + decodedPathname === `/all/catalog/${timeFilterName}/settings` || + decodedPathname === `/subscriptions/catalog` || + decodedPathname === `/subscriptions/catalog/settings` || + decodedPathname === `/subscriptions/catalog/${timeFilterName}` || + decodedPathname === `/subscriptions/catalog/${timeFilterName}/settings` || + decodedPathname === `/mod/catalog` || + decodedPathname === `/mod/catalog/settings` || + decodedPathname === `/mod/catalog/${timeFilterName}` || + decodedPathname === `/mod/catalog/${timeFilterName}/settings` ); }; export const isDescriptionView = (pathname: string, params: ParamsType): boolean => { const decodedPathname = decodeURIComponent(pathname); - return pathname === '/p/all/description' ? true : params.subplebbitAddress ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}/description`) : false; + const identifier = params.boardIdentifier || params.subplebbitAddress; + return pathname === '/all/description' ? true : identifier ? decodedPathname.startsWith(`/${identifier}/description`) : false; }; export const isHomeView = (pathname: string): boolean => { @@ -43,11 +62,11 @@ export const isHomeView = (pathname: string): boolean => { }; export const isModView = (pathname: string): boolean => { - return pathname === `/p/mod` || pathname.startsWith(`/p/mod/`); + return pathname === `/mod` || pathname.startsWith(`/mod/`); }; export const isPendingPostView = (pathname: string, params: ParamsType): boolean => { - return pathname === `/profile/${params.accountCommentIndex}` || pathname === `/profile/${params.accountCommentIndex}/settings`; + return pathname === `/pending/${params.accountCommentIndex}` || pathname === `/pending/${params.accountCommentIndex}/settings`; }; export const isPostPageView = (pathname: string, params: ParamsType): boolean => { @@ -55,36 +74,39 @@ export const isPostPageView = (pathname: string, params: ParamsType): boolean => return true; } const decodedPathname = decodeURIComponent(pathname); - return params.subplebbitAddress && params.commentCid ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}/c/${params.commentCid}`) : false; + const identifier = params.boardIdentifier || params.subplebbitAddress; + return identifier && params.commentCid ? decodedPathname.startsWith(`/${identifier}/thread/${params.commentCid}`) : false; }; export const isRulesView = (pathname: string, params: ParamsType): boolean => { const decodedPathname = decodeURIComponent(pathname); - return params.subplebbitAddress ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}/rules`) : false; + const identifier = params.boardIdentifier || params.subplebbitAddress; + return identifier ? decodedPathname.startsWith(`/${identifier}/rules`) : false; }; export const isSettingsView = (pathname: string, params: ParamsType): boolean => { - const { accountCommentIndex, commentCid, subplebbitAddress } = params; + const { accountCommentIndex, boardIdentifier, commentCid, subplebbitAddress } = params; + const identifier = boardIdentifier || subplebbitAddress; const decodedPathname = decodeURIComponent(pathname); return ( - decodedPathname === `/p/${subplebbitAddress}/c/${commentCid}/settings` || - decodedPathname === `/p/${subplebbitAddress}/description/settings` || - decodedPathname === `/p/${subplebbitAddress}/rules/settings` || - decodedPathname === `/profile/${accountCommentIndex}/settings` + (identifier && commentCid && decodedPathname === `/${identifier}/thread/${commentCid}/settings`) || + (identifier && decodedPathname === `/${identifier}/description/settings`) || + (identifier && decodedPathname === `/${identifier}/rules/settings`) || + decodedPathname === `/pending/${accountCommentIndex}/settings` ); }; export const isSubscriptionsView = (pathname: string, params: ParamsType): boolean => { const { timeFilterName } = params; return ( - pathname === '/p/subscriptions' || - pathname === '/p/subscriptions/settings' || - pathname === `/p/subscriptions/${timeFilterName}` || - pathname === `/p/subscriptions/${timeFilterName}/settings` || - pathname === '/p/subscriptions/catalog' || - pathname === '/p/subscriptions/catalog/settings' || - pathname === `/p/subscriptions/catalog/${timeFilterName}` || - pathname === `/p/subscriptions/catalog/${timeFilterName}/settings` + pathname === '/subscriptions' || + pathname === '/subscriptions/settings' || + pathname === `/subscriptions/${timeFilterName}` || + pathname === `/subscriptions/${timeFilterName}/settings` || + pathname === '/subscriptions/catalog' || + pathname === '/subscriptions/catalog/settings' || + pathname === `/subscriptions/catalog/${timeFilterName}` || + pathname === `/subscriptions/catalog/${timeFilterName}/settings` ); }; diff --git a/src/stores/use-disclaimer-modal-store.ts b/src/stores/use-disclaimer-modal-store.ts index 7815964d..cc0be158 100644 --- a/src/stores/use-disclaimer-modal-store.ts +++ b/src/stores/use-disclaimer-modal-store.ts @@ -6,7 +6,8 @@ export const DISCLAIMER_ACCEPTED_KEY = '5chan-disclaimer-accepted'; interface DisclaimerModalState { showModal: boolean; targetAddress: string | null; - showDisclaimerModal: (address: string, navigate: NavigateFunction) => void; + targetBoardPath: string | null; + showDisclaimerModal: (address: string, navigate: NavigateFunction, boardPath?: string) => void; closeDisclaimerModal: () => void; acceptDisclaimer: (navigate: NavigateFunction) => void; } @@ -30,12 +31,14 @@ const setDisclaimerAccepted = (): void => { const useDisclaimerModalStore = create((set) => ({ showModal: false, targetAddress: null, + targetBoardPath: null, - showDisclaimerModal: (address: string, navigate: NavigateFunction) => { + showDisclaimerModal: (address: string, navigate: NavigateFunction, boardPath?: string) => { // Check if user has already accepted the disclaimer if (hasAcceptedDisclaimer()) { // Navigate directly without showing modal - navigate(`/p/${address}`); + const path = boardPath || address; + navigate(`/${path}`); return; } @@ -43,6 +46,7 @@ const useDisclaimerModalStore = create((set) => ({ set({ showModal: true, targetAddress: address, + targetBoardPath: boardPath || null, }); }, @@ -50,6 +54,7 @@ const useDisclaimerModalStore = create((set) => ({ set({ showModal: false, targetAddress: null, + targetBoardPath: null, }); }, @@ -59,15 +64,17 @@ const useDisclaimerModalStore = create((set) => ({ // Save acceptance to localStorage setDisclaimerAccepted(); - // Navigate to the target address - if (state.targetAddress) { - navigate(`/p/${state.targetAddress}`); + // Navigate to the target board path (or address if no path) + const path = state.targetBoardPath || state.targetAddress; + if (path) { + navigate(`/${path}`); } // Close the modal set({ showModal: false, targetAddress: null, + targetBoardPath: null, }); }, })); diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx index 2672a19d..76d45fe6 100644 --- a/src/views/board/board.tsx +++ b/src/views/board/board.tsx @@ -8,6 +8,7 @@ import { shouldShowSnow } from '../../lib/snow'; import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils'; import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils'; import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; +import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address'; import { useFeedStateString } from '../../hooks/use-state-string'; import useTimeFilter from '../../hooks/use-time-filter'; import useInterfaceSettingsStore from '../../stores/use-interface-settings-store'; @@ -35,7 +36,8 @@ const createThreadsWithoutImagesFilter = () => ({ const Board = () => { const { t } = useTranslation(); const location = useLocation(); - const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>(); + const subplebbitAddress = useResolvedSubplebbitAddress(); + const boardPath = useBoardPath(subplebbitAddress); const { hideThreadsWithoutImages } = useInterfaceSettingsStore(); const isInAllView = isAllView(location.pathname); @@ -194,7 +196,7 @@ const Board = () => { i18nKey='more_threads_last_week' values={{ currentTimeFilterName, count: feed.length }} components={{ - 1: , + 1: , }} />
@@ -204,7 +206,7 @@ const Board = () => { i18nKey='more_threads_last_month' values={{ currentTimeFilterName, count: feed.length }} components={{ - 1: , + 1: , }} />
diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx index 382755a0..e3b6ab76 100644 --- a/src/views/catalog/catalog.tsx +++ b/src/views/catalog/catalog.tsx @@ -7,6 +7,7 @@ import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-util import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; import useCatalogFeedRows from '../../hooks/use-catalog-feed-rows'; import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; +import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address'; import { useFeedStateString } from '../../hooks/use-state-string'; import useTimeFilter from '../../hooks/use-time-filter'; import useWindowWidth from '../../hooks/use-window-width'; @@ -125,7 +126,8 @@ const createCombinedFilter = ( const Catalog = () => { const { t } = useTranslation(); const location = useLocation(); - const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>(); + const subplebbitAddress = useResolvedSubplebbitAddress(); + const boardPath = useBoardPath(subplebbitAddress); const isInAllView = isAllView(location.pathname); const defaultSubplebbits = useDefaultSubplebbits(); @@ -366,7 +368,7 @@ const Catalog = () => { i18nKey='more_threads_last_week' values={{ currentTimeFilterName, count: feed.length }} components={{ - 1: , + 1: , }} /> @@ -376,7 +378,7 @@ const Catalog = () => { i18nKey='more_threads_last_month' values={{ currentTimeFilterName, count: feed.length }} components={{ - 1: , + 1: , }} /> diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx index 121b9c7a..b4d81661 100644 --- a/src/views/home/boards-list/boards-list.tsx +++ b/src/views/home/boards-list/boards-list.tsx @@ -1,6 +1,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useDefaultSubplebbitsState, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits'; +import { useDefaultSubplebbitsState, useDefaultSubplebbits, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits'; +import { getBoardPath } from '../../../lib/utils/route-utils'; import LoadingEllipsis from '../../../components/loading-ellipsis'; import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store'; import useDirectoryModalStore from '../../../stores/use-directory-modal-store'; @@ -34,16 +35,19 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { const { showDisclaimerModal } = useDisclaimerModalStore(); const { openDirectoryModal } = useDirectoryModalStore(); const { useCatalogLinks, boardFilter } = useBoardsFilterStore(); + const defaultSubplebbits = useDefaultSubplebbits(); const handleLinkClick = (e: React.MouseEvent, address: string) => { e.preventDefault(); - showDisclaimerModal(address, navigate); + const boardPath = getBoardPath(address, defaultSubplebbits); + showDisclaimerModal(address, navigate, boardPath); }; // Helper to generate link URL with optional catalog suffix const getBoardLink = (address: string | null): string => { if (!address) return '#'; - return `/p/${address}${useCatalogLinks ? '/catalog' : ''}`; + const boardPath = getBoardPath(address, defaultSubplebbits); + return `/${boardPath}${useCatalogLinks ? '/catalog' : ''}`; }; // Handler for placeholder board links diff --git a/src/views/home/popular-threads-box/popular-threads-box.tsx b/src/views/home/popular-threads-box/popular-threads-box.tsx index 03effc0e..11b52aab 100644 --- a/src/views/home/popular-threads-box/popular-threads-box.tsx +++ b/src/views/home/popular-threads-box/popular-threads-box.tsx @@ -9,7 +9,8 @@ import { getCommentMediaInfo } from '../../../lib/utils/media-utils'; import { CatalogPostMedia } from '../../../components/catalog-row'; import LoadingEllipsis from '../../../components/loading-ellipsis'; import BoxModal from '../box-modal'; -import { MultisubSubplebbit } from '../../../hooks/use-default-subplebbits'; +import { MultisubSubplebbit, useDefaultSubplebbits } from '../../../hooks/use-default-subplebbits'; +import { getBoardPath } from '../../../lib/utils/route-utils'; import { removeMarkdown } from '../../../lib/utils/post-utils'; interface PopularThreadProps { @@ -27,16 +28,18 @@ export const ContentPreview = ({ content, maxLength = 99 }: { content: string; m const PopularThreadCard = ({ post, multisub }: PopularThreadProps) => { const { cid, content, link, linkHeight, linkWidth, subplebbitAddress, thumbnailUrl, title } = post || {}; const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); + const defaultSubplebbits = useDefaultSubplebbits(); // Find the matching MultisubSubplebbit entry and get its title const multisubEntry = multisub.find((ms) => ms?.address === subplebbitAddress); const boardTitle = multisubEntry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || ''; + const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; return (
{boardTitle}
- +
diff --git a/src/views/not-found/not-found.tsx b/src/views/not-found/not-found.tsx index fd2b56f2..0eb31a6b 100644 --- a/src/views/not-found/not-found.tsx +++ b/src/views/not-found/not-found.tsx @@ -13,7 +13,10 @@ const NotFoundImage = () => { const NotFound = () => { const location = useLocation(); - const subplebbitAddress = location.pathname.startsWith('/p/') ? location.pathname.split('/')[2] : ''; + // Extract boardIdentifier from pathname (could be directory code or address) + const pathParts = location.pathname.split('/').filter(Boolean); + const boardIdentifier = pathParts[0] && pathParts[0] !== 'not-found' && pathParts[0] !== 'faq' ? pathParts[0] : ''; + const subplebbitAddress = boardIdentifier || ''; const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]); const { address, shortAddress } = subplebbit || {}; @@ -32,7 +35,7 @@ const NotFound = () => { <>
- [Back to p/{shortAddress}] + [Back to p/{shortAddress}]
)} diff --git a/src/views/pending-post/pending-post.tsx b/src/views/pending-post/pending-post.tsx index e7f11c56..a381d1c6 100644 --- a/src/views/pending-post/pending-post.tsx +++ b/src/views/pending-post/pending-post.tsx @@ -1,6 +1,8 @@ import { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useAccountComment, useAccountComments } from '@plebbit/plebbit-react-hooks'; +import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; +import { getBoardPath } from '../../lib/utils/route-utils'; import { Post } from '../post'; const PendingPost = () => { @@ -9,6 +11,7 @@ const PendingPost = () => { const commentIndex = accountCommentIndex ? parseInt(accountCommentIndex) : undefined; const post = useAccountComment({ commentIndex }); const navigate = useNavigate(); + const defaultSubplebbits = useDefaultSubplebbits(); useEffect(() => window.scrollTo(0, 0), []); @@ -28,9 +31,10 @@ const PendingPost = () => { useEffect(() => { if (post?.cid && post?.subplebbitAddress) { - navigate(`/p/${post?.subplebbitAddress}/c/${post?.cid}`, { replace: true }); + const boardPath = getBoardPath(post.subplebbitAddress, defaultSubplebbits); + navigate(`/${boardPath}/thread/${post.cid}`, { replace: true }); } - }, [post, navigate]); + }, [post, navigate, defaultSubplebbits]); return ; }; diff --git a/src/views/post/post.tsx b/src/views/post/post.tsx index e1676fd4..c2953fba 100644 --- a/src/views/post/post.tsx +++ b/src/views/post/post.tsx @@ -4,6 +4,7 @@ import { Comment, Role, useComment, useEditedComment, useSubplebbit } from '@ple import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import { useLocation, useParams } from 'react-router-dom'; import { isAllView, isDescriptionView, isRulesView } from '../../lib/utils/view-utils'; +import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; import useIsMobile from '../../hooks/use-is-mobile'; import ErrorDisplay from '../../components/error-display/error-display'; import PostDesktop from '../../components/post-desktop'; @@ -53,7 +54,8 @@ const PostPage = () => { const { t } = useTranslation(); const params = useParams(); const location = useLocation(); - const { commentCid, subplebbitAddress } = params; + const { commentCid } = params; + const subplebbitAddress = useResolvedSubplebbitAddress(); const isInAllView = isAllView(location.pathname); const isInDescriptionView = isDescriptionView(location.pathname, params); const isInRulesView = isRulesView(location.pathname, params); @@ -101,7 +103,7 @@ const PostPage = () => { avatarUrl={suggested?.avatarUrl} createdAt={createdAt} description={description} - replyCount={location.pathname.startsWith('/p/all/') ? 0 : rules?.length > 0 ? 1 : 0} + replyCount={location.pathname.startsWith('/all/') ? 0 : rules?.length > 0 ? 1 : 0} subplebbitAddress={subplebbitAddress} shortAddress={shortAddress} title={title}