-
{' '}
-
-
- {t('stored_locally', {
- location: window.electronApi?.isElectron ? 'this desktop app' : isAndroid ? 'this mobile app' : window.location.hostname,
- interpolation: { escapeValue: false },
- })}
+ {' '}
+
{' '}
+
+
+ {t('account_auto_generated')} {t('stored_locally', { location: host, interpolation: { escapeValue: false } })}
-
+
diff --git a/src/hooks/use-popular-posts.ts b/src/hooks/use-popular-posts.ts
index 534e2cf4..63a21902 100644
--- a/src/hooks/use-popular-posts.ts
+++ b/src/hooks/use-popular-posts.ts
@@ -2,77 +2,92 @@ import { useMemo, useRef } from 'react';
import { Comment, Subplebbit } from '@plebbit/plebbit-react-hooks';
import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils';
+const MAX_POSTS = 8;
+const MAX_PER_SUB = 3;
+
/**
- * Extracts popular posts from subplebbits.
- * Uses memoization to avoid recomputing when only updatingState changes.
+ * Ranked by replyCount instead of a static threshold so the box
+ * adapts to both low- and high-activity periods.
+ *
+ * Grow-only commit: once a post enters the grid it never shifts or
+ * disappears — new posts fill remaining slots until the cap is reached.
*/
const usePopularPosts = (subplebbits: Subplebbit[]) => {
- // Track the previous CID list to detect actual content changes vs transient state changes
- const prevCidsRef = useRef('');
+ const committedRef = useRef<{ posts: Comment[]; cids: Set }>({
+ posts: [],
+ cids: new Set(),
+ });
+ const prevInputKeyRef = useRef('');
+
+ // Reset committed when the board set changes (e.g. NSFW filter toggle)
+ const inputKey = subplebbits
+ .map((s) => s?.address)
+ .filter(Boolean)
+ .sort()
+ .join(',');
+ if (prevInputKeyRef.current !== inputKey) {
+ prevInputKeyRef.current = inputKey;
+ committedRef.current = { posts: [], cids: new Set() };
+ }
+
+ const candidates = useMemo(() => {
+ if (committedRef.current.posts.length >= MAX_POSTS) return [];
- const { popularPosts, error } = useMemo(() => {
try {
- const uniqueLinks: Set = new Set();
+ const uniqueLinks = new Set();
const allPosts: Comment[] = [];
- // Base quota on boards that currently have loaded hot comments.
- // Using total directory count can underfill the list when many boards are empty/unavailable.
- const loadedSubplebbitsCount = subplebbits.filter((sub) => sub?.posts?.pages?.hot?.comments).length;
- const postsPerSub = [0, 8, 4, 3, 2, 2, 2, 2, 1][Math.min(loadedSubplebbitsCount, 8)];
+ for (const sub of subplebbits) {
+ if (!sub?.posts?.pages?.hot?.comments) continue;
- subplebbits.forEach((subplebbit: any) => {
- let subplebbitPosts: Comment[] = [];
+ const subPosts: Comment[] = [];
+ for (const post of Object.values(sub.posts.pages.hot.comments as Comment)) {
+ const { deleted, link, linkHeight, linkWidth, locked, pinned, removed, thumbnailUrl } = post;
- if (subplebbit?.posts?.pages?.hot?.comments) {
- const rawPosts = Object.values(subplebbit.posts.pages.hot.comments as Comment);
- for (const post of rawPosts) {
- const { deleted, link, linkHeight, linkWidth, locked, pinned, removed, replyCount, thumbnailUrl } = post;
+ try {
+ const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
+ const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
- try {
- const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
- const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
-
- if (hasThumbnail && replyCount > 1 && !deleted && !removed && !locked && !pinned && !uniqueLinks.has(link)) {
- subplebbitPosts.push(post);
- uniqueLinks.add(link);
- }
- } catch (err) {
- console.error('Error processing post:', err);
+ if (hasThumbnail && !deleted && !removed && !locked && !pinned && !uniqueLinks.has(link)) {
+ subPosts.push(post);
+ uniqueLinks.add(link);
}
+ } catch {
+ // skip posts with malformed media URLs
}
-
- subplebbitPosts.sort((a: any, b: any) => b.timestamp - a.timestamp);
- const selectedPosts = subplebbitPosts.slice(0, postsPerSub);
- allPosts.push(...selectedPosts);
}
+
+ subPosts.sort((a, b) => (b.replyCount ?? 0) - (a.replyCount ?? 0));
+ allPosts.push(...subPosts.slice(0, MAX_PER_SUB));
+ }
+
+ // Primary: most replies first. Tiebreaker: newest first.
+ allPosts.sort((a, b) => {
+ const diff = (b.replyCount ?? 0) - (a.replyCount ?? 0);
+ return diff !== 0 ? diff : (b.timestamp ?? 0) - (a.timestamp ?? 0);
});
- const sortedPosts = allPosts.sort((a: any, b: any) => b.timestamp - a.timestamp).slice(0, 8);
-
- return { popularPosts: sortedPosts, error: null };
+ return allPosts;
} catch (err) {
console.error('Error in usePopularPosts:', err);
- return { popularPosts: [], error: 'Failed to fetch popular posts' };
+ return [];
}
}, [subplebbits]);
- // Create stable reference: only update if the post content actually changes
- // Build a key from relevant mutable fields, not just CIDs
- const currentKey = popularPosts.map((p) => `${p.cid}:${p.replyCount}:${p.timestamp}:${p.locked}:${p.pinned}`).join(',');
- const stablePostsRef = useRef(popularPosts);
- const keyChanged = currentKey !== prevCidsRef.current;
-
- if (keyChanged) {
- prevCidsRef.current = currentKey;
- stablePostsRef.current = popularPosts;
+ // Grow-only: committed posts keep their position, new ones fill empty slots
+ const { posts, cids } = committedRef.current;
+ for (const post of candidates) {
+ if (posts.length >= MAX_POSTS) break;
+ if (post.cid && !cids.has(post.cid)) {
+ posts.push(post);
+ cids.add(post.cid);
+ }
}
- // Derive loading state from subplebbit states rather than post count
- // A subplebbit is still loading if it has no posts pages yet and isn't in a terminal state
const hasLoadedData = subplebbits.some((sub) => sub?.posts?.pages?.hot?.comments);
const isLoading = subplebbits.length > 0 && !hasLoadedData;
- return { popularPosts: stablePostsRef.current, isLoading, error };
+ return { popularPosts: posts, isLoading, error: null as string | null };
};
export default usePopularPosts;
diff --git a/src/index.css b/src/index.css
index 97e46b19..0d53e141 100644
--- a/src/index.css
+++ b/src/index.css
@@ -82,7 +82,8 @@ hr {
cursor: pointer;
}
- .highlight {
+ .highlight,
+ .scroll-highlight {
background: var(--reply-highlight-background-color) !important;
}
@@ -104,7 +105,8 @@ hr {
cursor: pointer;
}
- .highlight {
+ .highlight,
+ .scroll-highlight {
background: var(--reply-highlight-background-color) !important;
border: var(--reply-highlight-border) !important;
border-left: var(--reply-highlight-border-left, revert) !important;
diff --git a/src/themes.css b/src/themes.css
index 713bd5cd..fbba8575 100644
--- a/src/themes.css
+++ b/src/themes.css
@@ -58,7 +58,7 @@
--hr-height: 0px;
/* pagelist */
- --pagelist-background-color: #ead6ca;
+ --pagelist-background-color: #F0E0D6;
--pagelist-item-background-color: #f0e0d6;
--pagelist-item-border-color: #d9bfb7;
--pagelist-item-text-color: #800;
@@ -303,7 +303,7 @@
--footer-item-background-color-desktop: #eef2ff;
/* pagelist */
- --pagelist-background-color: #c9cde8;
+ --pagelist-background-color: #D6DAF0;
--pagelist-item-background-color: #d6daf0;
--pagelist-item-border-color: #b7c5d9;
--pagelist-item-text-color: #89a;
diff --git a/src/views/account-data-editor/account-data-editor.tsx b/src/views/account-data-editor/account-data-editor.tsx
index 8f61b806..b753b90d 100644
--- a/src/views/account-data-editor/account-data-editor.tsx
+++ b/src/views/account-data-editor/account-data-editor.tsx
@@ -8,8 +8,11 @@ import styles from './account-data-editor.module.css';
const DEFAULT_RETURN_TO = '/subs/settings#account-settings';
const loadAce = async () => {
- const [aceModule] = await Promise.all([import('react-ace'), import('ace-builds/src-noconflict/mode-json'), import('ace-builds/src-noconflict/theme-monokai')]);
- return aceModule.default;
+ const aceModule = await import('react-ace');
+ await Promise.all([import('ace-builds/src-noconflict/mode-json'), import('ace-builds/src-noconflict/theme-monokai')]);
+ // Vite CJS interop can double-wrap the default export
+ const mod = aceModule.default;
+ return typeof mod === 'function' ? mod : (mod as unknown as { default: typeof mod }).default;
};
const AccountDataEditor = () => {
@@ -103,7 +106,7 @@ const AccountDataEditor = () => {
)}
-
post?.cid || `post-${index}`}
- itemContent={(index, post) => }
- useWindowScroll={true}
- components={footerComponents}
- endReached={effectiveInfiniteScroll && hasMore ? loadMore : undefined}
- ref={virtuosoRef}
- restoreStateFrom={lastVirtuosoState}
- initialScrollTop={lastVirtuosoState?.scrollTop}
- />
+ {effectiveInfiniteScroll ? (
+ post?.cid || `post-${index}`}
+ itemContent={(index, post) => }
+ useWindowScroll={true}
+ components={footerComponents}
+ endReached={hasMore ? loadMore : undefined}
+ ref={virtuosoRef}
+ restoreStateFrom={lastVirtuosoState}
+ initialScrollTop={lastVirtuosoState?.scrollTop}
+ />
+ ) : (
+ <>
+ {displayFeed.map((post, index) => (
+
+ ))}
+
+ >
+ )}
>
);
diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx
index 9aacca7b..ca429838 100644
--- a/src/views/catalog/catalog.tsx
+++ b/src/views/catalog/catalog.tsx
@@ -12,7 +12,6 @@ import { useFeedStateString } from '../../hooks/use-state-string';
import useWindowWidth from '../../hooks/use-window-width';
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
import useFeedResetStore from '../../stores/use-feed-reset-store';
-import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store';
import useSortingStore from '../../stores/use-sorting-store';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import { getSubplebbitAddress, isDirectoryBoard, normalizeMultiboardFeedPath } from '../../lib/utils/route-utils';
@@ -204,9 +203,9 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
const isInSubscriptionsView = viewType ? viewType === 'subs' : false;
const isInModView = viewType ? viewType === 'mod' : false;
- const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
- const isForcedInfiniteScroll = isInAllView || isInSubscriptionsView || isInModView;
- const effectiveInfiniteScroll = enableInfiniteScroll || isForcedInfiniteScroll;
+ const isMultiboard = isInAllView || isInSubscriptionsView || isInModView;
+ // Single-board catalogs always cap at maxGuiPages (no infinite scroll beyond the board's page limit)
+ const effectiveInfiniteScroll = isMultiboard;
const directories = useDirectories();
const resolvedAddressFromUrl = useResolvedSubplebbitAddress();
@@ -241,7 +240,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
const postsPerPage = columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25;
const community = useDirectoryByAddress(isInAllView || isInSubscriptionsView || isInModView ? undefined : subplebbitAddress);
- const { guiPostsPerPage: boardPostsPerPage, maxGuiPages, paginationFeedPostsPerPage, infiniteFeedPostsPerPage } = useBoardFeedPageSize(community);
+ const { guiPostsPerPage: boardPostsPerPage, maxGuiPages, paginationFeedPostsPerPage } = useBoardFeedPageSize(community);
// Canonical redirect for multiboard catalog paths with numeric page segment (e.g. /all/catalog/1w/5 -> /all/catalog/1w)
useEffect(() => {
@@ -269,35 +268,13 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
}, [subplebbitAddress]);
const feedOptions = useMemo(() => {
- const catalogPostsPerPage =
- isInAllView || isInSubscriptionsView || isInModView
- ? effectiveInfiniteScroll
- ? 10
- : 100
- : effectiveInfiniteScroll
- ? infiniteFeedPostsPerPage
- : paginationFeedPostsPerPage;
-
return {
subplebbitAddresses,
sortType: feedSortType,
- postsPerPage: catalogPostsPerPage,
+ postsPerPage: isMultiboard ? 10 : paginationFeedPostsPerPage,
filter: createCombinedFilter(filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch),
};
- }, [
- subplebbitAddresses,
- feedSortType,
- isInAllView,
- isInSubscriptionsView,
- isInModView,
- effectiveInfiniteScroll,
- infiniteFeedPostsPerPage,
- paginationFeedPostsPerPage,
- filterItems,
- searchText,
- subplebbitAddress,
- handleFilterMatch,
- ]);
+ }, [subplebbitAddresses, feedSortType, isMultiboard, paginationFeedPostsPerPage, filterItems, searchText, subplebbitAddress, handleFilterMatch]);
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
const { accountComments } = useAccountComments();
diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx
index 1972cad7..1410a8eb 100644
--- a/src/views/home/boards-list/boards-list.tsx
+++ b/src/views/home/boards-list/boards-list.tsx
@@ -1,5 +1,6 @@
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
import { useDirectoriesState, useDirectories, DirectoryCommunity } from '../../../hooks/use-directories';
import { getBoardPath } from '../../../lib/utils/route-utils';
import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store';
@@ -63,6 +64,15 @@ const BoardsList = ({ multisub }: { multisub: DirectoryCommunity[] }) => {
const { useCatalogLinks, boardFilter } = useBoardsFilterStore();
const directories = useDirectories();
+ const accountSubplebbitAddresses = useAccountsStore(
+ (state) => {
+ const activeAccountId = state.activeAccountId;
+ const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
+ return Object.keys(activeAccount?.subplebbits || {});
+ },
+ (prev, next) => prev.length === next.length && prev.every((val, idx) => val === next[idx]),
+ );
+
const handleLinkClick = (e: React.MouseEvent
, address: string) => {
e.preventDefault();
const boardPath = getBoardPath(address, directories);
@@ -595,6 +605,11 @@ const BoardsList = ({ multisub }: { multisub: DirectoryCommunity[] }) => {
Subscriptions
+ {accountSubplebbitAddresses.length > 0 && (
+
+ {t('boards_you_moderate_nav')}
+
+ )}
)}
diff --git a/src/views/home/box-modal/box-modal.tsx b/src/views/home/box-modal/box-modal.tsx
index 609770d1..92ad4e10 100644
--- a/src/views/home/box-modal/box-modal.tsx
+++ b/src/views/home/box-modal/box-modal.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import useHomeFiltersStore from '../../../stores/use-popular-threads-options-store';
import styles from '../home.module.css';
@@ -11,21 +11,37 @@ const BoxModal = () => {
const { showWorksafeContentOnly, setShowWorksafeContentOnly, showNsfwContentOnly, setShowNsfwContentOnly } = useHomeFiltersStore();
- const handleClickOutside = useCallback(
- (event: MouseEvent) => {
+ useEffect(() => {
+ if (!showFilterModal) return;
+ const handleClickOutside = (event: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(event.target as Node) && buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
setShowFilterModal(false);
}
- },
- [modalRef, buttonRef, setShowFilterModal],
- );
-
- useEffect(() => {
- document.addEventListener('mousedown', handleClickOutside);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
};
- }, [handleClickOutside]);
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, [showFilterModal]);
+
+ const selectFilter = (filter: 'worksafe' | 'nsfw' | 'all') => {
+ if (filter === 'worksafe') {
+ if (showNsfwContentOnly) setShowNsfwContentOnly(false);
+ setShowWorksafeContentOnly(!showWorksafeContentOnly);
+ } else if (filter === 'nsfw') {
+ if (showWorksafeContentOnly) setShowWorksafeContentOnly(false);
+ setShowNsfwContentOnly(!showNsfwContentOnly);
+ } else {
+ setShowWorksafeContentOnly(false);
+ setShowNsfwContentOnly(false);
+ }
+ setShowFilterModal(false);
+ };
+
+ const handleKey = (e: React.KeyboardEvent, filter: 'worksafe' | 'nsfw' | 'all') => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ selectFilter(filter);
+ }
+ };
return (
<>
@@ -46,70 +62,29 @@ const BoxModal = () => {
{showFilterModal && (
{
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- if (showNsfwContentOnly) {
- setShowNsfwContentOnly(false);
- }
- setShowWorksafeContentOnly(!showWorksafeContentOnly);
- setShowFilterModal(false);
- }
- }}
- onClick={() => {
- if (showNsfwContentOnly) {
- setShowNsfwContentOnly(false);
- }
- setShowWorksafeContentOnly(!showWorksafeContentOnly);
- setShowFilterModal(false);
- }}
+ onKeyDown={(e) => handleKey(e, 'worksafe')}
+ onClick={() => selectFilter('worksafe')}
>
{t('show_worksafe_content_only')}
{
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- if (showWorksafeContentOnly) {
- setShowWorksafeContentOnly(false);
- }
- setShowNsfwContentOnly(!showNsfwContentOnly);
- setShowFilterModal(false);
- }
- }}
- onClick={() => {
- if (showWorksafeContentOnly) {
- setShowWorksafeContentOnly(false);
- }
- setShowNsfwContentOnly(!showNsfwContentOnly);
- setShowFilterModal(false);
- }}
+ onKeyDown={(e) => handleKey(e, 'nsfw')}
+ onClick={() => selectFilter('nsfw')}
>
{t('show_nsfw_content_only')}
{
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- setShowWorksafeContentOnly(false);
- setShowNsfwContentOnly(false);
- setShowFilterModal(false);
- }
- }}
- onClick={() => {
- setShowWorksafeContentOnly(false);
- setShowNsfwContentOnly(false);
- setShowFilterModal(false);
- }}
+ onKeyDown={(e) => handleKey(e, 'all')}
+ onClick={() => selectFilter('all')}
>
{t('show_all_content')}
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 0e176a05..7cb215ff 100644
--- a/src/views/home/popular-threads-box/popular-threads-box.tsx
+++ b/src/views/home/popular-threads-box/popular-threads-box.tsx
@@ -15,7 +15,8 @@ import { removeMarkdown } from '../../../lib/utils/post-utils';
interface PopularThreadProps {
post: Comment;
- directories: DirectoryCommunity[];
+ boardTitle: string;
+ boardPath: string;
}
export const ContentPreview = ({ content, maxLength = 99 }: { content: string; maxLength?: number }) => {
@@ -25,23 +26,17 @@ export const ContentPreview = ({ content, maxLength = 99 }: { content: string; m
return truncatedText;
};
-// Memoize to prevent rerenders when parent rerenders due to updatingState
const PopularThreadCard = memo(
- ({ post, directories }: PopularThreadProps) => {
- const { cid, content, link, linkHeight, linkWidth, subplebbitAddress, thumbnailUrl, title } = post || {};
+ ({ post, boardTitle, boardPath }: PopularThreadProps) => {
+ const { cid, content, link, linkHeight, linkWidth, thumbnailUrl, title } = post || {};
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
- // Find the matching DirectoryCommunity entry and get its title
- const directoriesEntry = directories.find((ms) => ms?.address === subplebbitAddress);
- const boardTitle = directoriesEntry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
- const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : '';
-
return (
{boardTitle}
-
+
@@ -56,35 +51,31 @@ const PopularThreadCard = memo(
);
},
- // Custom equality: rerender if post.cid or directories entry title changes
- (prevProps, nextProps) => {
- if (prevProps.post?.cid !== nextProps.post?.cid) return false;
- // Compare the relevant directories entry for this post's subplebbitAddress
- const prevEntry = prevProps.directories.find((ms) => ms?.address === prevProps.post?.subplebbitAddress);
- const nextEntry = nextProps.directories.find((ms) => ms?.address === nextProps.post?.subplebbitAddress);
- return prevEntry?.title === nextEntry?.title;
- },
+ (prevProps, nextProps) => prevProps.post?.cid === nextProps.post?.cid && prevProps.boardTitle === nextProps.boardTitle,
);
const PopularThreadsBox = ({ directories, subplebbits }: { directories: DirectoryCommunity[]; subplebbits: any }) => {
const { t } = useTranslation();
const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore();
+ const directoryByAddress = useMemo(() => new Map(directories.map((d) => [d.address, d])), [directories]);
+
const filteredSubplebbits = useMemo(() => {
if (showWorksafeContentOnly) {
return subplebbits.filter((sub: Subplebbit) => {
- const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
- return directoriesEntry ? !directoriesEntry.nsfw : true;
+ const entry = directoryByAddress.get(sub?.address);
+ return entry ? !entry.nsfw : true;
});
}
if (showNsfwContentOnly) {
return subplebbits.filter((sub: Subplebbit) => {
- const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
- return directoriesEntry ? directoriesEntry.nsfw : false;
+ const entry = directoryByAddress.get(sub?.address);
+ return entry ? entry.nsfw : false;
});
}
return subplebbits;
- }, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, directories]);
+ }, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, directoryByAddress]);
+
const { popularPosts } = usePopularPosts(filteredSubplebbits);
const isLoading = popularPosts.length === 0;
@@ -98,7 +89,12 @@ const PopularThreadsBox = ({ directories, subplebbits }: { directories: Director
{isLoading ? (
) : (
- popularPosts.map((post: any) =>
)
+ popularPosts.map((post: Comment) => {
+ const entry = directoryByAddress.get(post.subplebbitAddress);
+ const boardTitle = entry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
+ const boardPath = post.subplebbitAddress ? getBoardPath(post.subplebbitAddress, directories) : '';
+ return
;
+ })
)}
diff --git a/yarn.lock b/yarn.lock
index ebe83f19..ef506b9b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4581,6 +4581,78 @@
viem "2.45.0"
zod "4.3.6"
+"@plebbit/plebbit-js@https://github.com/plebbit/plebbit-js.git#fd7486d31ce71b29b6dc49c019278af8fb0b0d45":
+ version "0.0.11"
+ resolved "https://github.com/plebbit/plebbit-js.git#fd7486d31ce71b29b6dc49c019278af8fb0b0d45"
+ dependencies:
+ "@bonfida/spl-name-service" "3.0.16"
+ "@enhances/with-resolvers" "0.0.5"
+ "@helia/block-brokers" "5.0.13"
+ "@helia/delegated-routing-v1-http-api-client" "5.1.4"
+ "@helia/ipns" "9.1.5"
+ "@helia/unixfs" "6.0.4"
+ "@libp2p/crypto" "5.1.13"
+ "@libp2p/fetch" "4.0.9"
+ "@libp2p/gossipsub" "15.0.11"
+ "@libp2p/identify" "4.0.9"
+ "@libp2p/interfaces" "3.3.2"
+ "@libp2p/peer-id" "6.0.4"
+ "@libp2p/peer-id-factory" "4.2.4"
+ "@libp2p/websockets" "10.1.2"
+ "@mintpass/challenge" "1.1.11"
+ "@plebbit/plebbit-logger" "git+https://github.com/plebbit/plebbit-logger.git#355a96d7659ed820047980049dfa627d30d83a69"
+ "@plebbit/proper-lockfile" "git+https://github.com/plebbit/node-proper-lockfile.git#7fd6332117340c1d3d98dd0afee2d31cc06f72b8"
+ "@types/proper-lockfile" "4.1.2"
+ assert "2.1.0"
+ better-sqlite3 "12.4.1"
+ blockstore-core "6.1.1"
+ blockstore-fs "3.0.2"
+ blockstore-idb "3.0.1"
+ buffer "6.0.3"
+ captcha-canvas "3.3.4"
+ cbor "9.0.1"
+ debounce "1.2.1"
+ err-code "3.0.1"
+ ethers "6.16.0"
+ ext-name "5.0.0"
+ helia "6.0.14"
+ hpagent "1.2.0"
+ it-all "3.0.6"
+ jose "4.11.0"
+ js-sha256 "0.11.1"
+ js-sha512 "0.9.0"
+ kubo-rpc-client "6.0.2"
+ limiter-es6-compat "2.1.2"
+ localforage "1.10.0"
+ lodash.merge "4.6.2"
+ lru-cache "10.1.0"
+ open-graph-scraper "6.10.0"
+ p-limit "4.0.0"
+ p-timeout "6.1.2"
+ peer-id "0.16.0"
+ probe-image-size "7.2.3"
+ remeda "1.57.0"
+ retry "0.13.1"
+ rpc-websockets "9.3.1"
+ safe-stable-stringify "2.4.3"
+ sha1-uint8array "0.10.7"
+ skia-canvas "3.0.8"
+ tcp-port-used "1.0.2"
+ tiny-typed-emitter "2.1.0"
+ tinycache "1.1.2"
+ ts-custom-error "3.3.1"
+ typestub-ipfs-only-hash "4.0.0"
+ uuid "11.1.0"
+ viem "2.45.0"
+ zod "4.3.6"
+
+"@plebbit/plebbit-logger@git+https://github.com/plebbit/plebbit-logger.git#355a96d7659ed820047980049dfa627d30d83a69":
+ version "0.0.1"
+ uid "355a96d7659ed820047980049dfa627d30d83a69"
+ resolved "git+https://github.com/plebbit/plebbit-logger.git#355a96d7659ed820047980049dfa627d30d83a69"
+ dependencies:
+ debug "4.3.4"
+
"@plebbit/plebbit-logger@github:plebbit/plebbit-logger#355a96d7659ed820047980049dfa627d30d83a69":
version "0.0.1"
uid "355a96d7659ed820047980049dfa627d30d83a69"
@@ -4610,6 +4682,30 @@
uuid "11.1.0"
zustand "4.0.0"
+"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#a828bd598bcc392ed379136feaee0a7b1c8cd23f":
+ version "0.0.1"
+ resolved "https://github.com/plebbit/plebbit-react-hooks.git#a828bd598bcc392ed379136feaee0a7b1c8cd23f"
+ dependencies:
+ "@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#fd7486d31ce71b29b6dc49c019278af8fb0b0d45"
+ "@plebbit/plebbit-logger" "https://github.com/plebbit/plebbit-logger.git"
+ assert "2.0.0"
+ ethers "5.6.9"
+ localforage "1.10.0"
+ lodash.isequal "4.5.0"
+ memoizee "0.4.15"
+ quick-lru "5.1.1"
+ uint8arrays "3.1.1"
+ uuid "11.1.0"
+ zustand "4.0.0"
+
+"@plebbit/proper-lockfile@git+https://github.com/plebbit/node-proper-lockfile.git#7fd6332117340c1d3d98dd0afee2d31cc06f72b8":
+ version "4.1.2"
+ resolved "git+https://github.com/plebbit/node-proper-lockfile.git#7fd6332117340c1d3d98dd0afee2d31cc06f72b8"
+ dependencies:
+ graceful-fs "^4.2.4"
+ retry "^0.12.0"
+ signal-exit "^3.0.2"
+
"@plebbit/proper-lockfile@github:plebbit/node-proper-lockfile#7fd6332117340c1d3d98dd0afee2d31cc06f72b8":
version "4.1.2"
resolved "https://codeload.github.com/plebbit/node-proper-lockfile/tar.gz/7fd6332117340c1d3d98dd0afee2d31cc06f72b8"