feat(thread-stats): show board page number in post stats via cache-first lookup

This commit is contained in:
plebeius
2026-02-24 18:15:50 +08:00
parent bec6bf4d51
commit 0ddb22f5fa
38 changed files with 239 additions and 35 deletions
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "إظهار لوحة الإعلانات",
"top": "أعلى",
"reply_count": "عدد الردود",
"pagination.pageCurrent": "الصفحة {{page}} (الحالية)"
"pagination.pageCurrent": "الصفحة {{page}} (الحالية)",
"pagination.pageLabel": "صفحة"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "ঘোষণা দেখান",
"top": "শীর্ষ",
"reply_count": "উত্তরসংখ্যা",
"pagination.pageCurrent": "পৃষ্ঠা {{page}} (বর্তমান)"
"pagination.pageCurrent": "পৃষ্ঠা {{page}} (বর্তমান)",
"pagination.pageLabel": "পৃষ্ঠা"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Zobrazit Oznámení",
"top": "Nahoru",
"reply_count": "Počet odpovědí",
"pagination.pageCurrent": "Stránka {{page}} (aktuální)"
"pagination.pageCurrent": "Stránka {{page}} (aktuální)",
"pagination.pageLabel": "Stránka"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Vis Meddelelser",
"top": "Top",
"reply_count": "Antal svar",
"pagination.pageCurrent": "Side {{page}} (nuværende)"
"pagination.pageCurrent": "Side {{page}} (nuværende)",
"pagination.pageLabel": "Side"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Ankündigungen anzeigen",
"top": "Oben",
"reply_count": "Anzahl Antworten",
"pagination.pageCurrent": "Seite {{page}} (aktuell)"
"pagination.pageCurrent": "Seite {{page}} (aktuell)",
"pagination.pageLabel": "Seite"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Εμφάνιση Ανακοινώσεων",
"top": "Πάνω",
"reply_count": "Αριθμός απαντήσεων",
"pagination.pageCurrent": "Σελίδα {{page}} (τρέχουσα)"
"pagination.pageCurrent": "Σελίδα {{page}} (τρέχουσα)",
"pagination.pageLabel": "Σελίδα"
}
+2 -1
View File
@@ -282,5 +282,6 @@
"message": "Message",
"show_all": "Show All",
"show_blotter": "Show Blotter",
"reply_count": "Reply count"
"reply_count": "Reply count",
"pagination.pageLabel": "Page"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Mostrar Anuncios",
"top": "Arriba",
"reply_count": "Número de respuestas",
"pagination.pageCurrent": "Página {{page}} (actual)"
"pagination.pageCurrent": "Página {{page}} (actual)",
"pagination.pageLabel": "Página"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "نمایش اعلانات",
"top": "بالا",
"reply_count": "تعداد پاسخ‌ها",
"pagination.pageCurrent": "صفحه {{page}} (فعلی)"
"pagination.pageCurrent": "صفحه {{page}} (فعلی)",
"pagination.pageLabel": "صفحه"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Näytä ilmoitukset",
"top": "Ylös",
"reply_count": "Vastausten määrä",
"pagination.pageCurrent": "Sivu {{page}} (nykyinen)"
"pagination.pageCurrent": "Sivu {{page}} (nykyinen)",
"pagination.pageLabel": "Sivu"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Ipakita ang Mga anunsyo",
"top": "Itaas",
"reply_count": "Bilang ng mga tugon",
"pagination.pageCurrent": "Pahina {{page}} (kasalukuyan)"
"pagination.pageCurrent": "Pahina {{page}} (kasalukuyan)",
"pagination.pageLabel": "Pahina"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Afficher les annonces",
"top": "Haut",
"reply_count": "Nombre de réponses",
"pagination.pageCurrent": "Page {{page}} (actuelle)"
"pagination.pageCurrent": "Page {{page}} (actuelle)",
"pagination.pageLabel": "Page"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "הצג הודעות",
"top": "למעלה",
"reply_count": "מספר תגובות",
"pagination.pageCurrent": "עמוד {{page}} (נוכחי)"
"pagination.pageCurrent": "עמוד {{page}} (נוכחי)",
"pagination.pageLabel": "עמוד"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "सूचनाएं दिखाएं",
"top": "शीर्ष",
"reply_count": "जवाबों की संख्या",
"pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)"
"pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)",
"pagination.pageLabel": "पृष्ठ"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Közlemények megjelenítése",
"top": "Felül",
"reply_count": "Válaszok száma",
"pagination.pageCurrent": "{{page}}. oldal (jelenlegi)"
"pagination.pageCurrent": "{{page}}. oldal (jelenlegi)",
"pagination.pageLabel": "Oldal"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Tampilkan Pengumuman",
"top": "Atas",
"reply_count": "Jumlah balasan",
"pagination.pageCurrent": "Halaman {{page}} (saat ini)"
"pagination.pageCurrent": "Halaman {{page}} (saat ini)",
"pagination.pageLabel": "Halaman"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Mostra annunci",
"top": "In alto",
"reply_count": "Numero di risposte",
"pagination.pageCurrent": "Pagina {{page}} (corrente)"
"pagination.pageCurrent": "Pagina {{page}} (corrente)",
"pagination.pageLabel": "Pagina"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "お知らせを表示",
"top": "上",
"reply_count": "返信数",
"pagination.pageCurrent": "{{page}}ページ(現在)"
"pagination.pageCurrent": "{{page}}ページ(現在)",
"pagination.pageLabel": "ページ"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "공지 표시",
"top": "맨 위",
"reply_count": "답글 수",
"pagination.pageCurrent": "{{page}}페이지 (현재)"
"pagination.pageCurrent": "{{page}}페이지 (현재)",
"pagination.pageLabel": "페이지"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "जाहिराती दाखवा",
"top": "वर",
"reply_count": "प्रत्युत्तरांची संख्या",
"pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)"
"pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)",
"pagination.pageLabel": "पृष्ठ"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Mededelingen tonen",
"top": "Boven",
"reply_count": "Aantal reacties",
"pagination.pageCurrent": "Pagina {{page}} (huidige)"
"pagination.pageCurrent": "Pagina {{page}} (huidige)",
"pagination.pageLabel": "Pagina"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Vis Kunngjøringer",
"top": "Topp",
"reply_count": "Antall svar",
"pagination.pageCurrent": "Side {{page}} (nåværende)"
"pagination.pageCurrent": "Side {{page}} (nåværende)",
"pagination.pageLabel": "Side"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Pokaż Ogłoszenia",
"top": "Góra",
"reply_count": "Liczba odpowiedzi",
"pagination.pageCurrent": "Strona {{page}} (bieżąca)"
"pagination.pageCurrent": "Strona {{page}} (bieżąca)",
"pagination.pageLabel": "Strona"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Mostrar Anúncios",
"top": "Topo",
"reply_count": "Número de respostas",
"pagination.pageCurrent": "Página {{page}} (atual)"
"pagination.pageCurrent": "Página {{page}} (atual)",
"pagination.pageLabel": "Página"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Afișează Blotter",
"top": "Sus",
"reply_count": "Număr de răspunsuri",
"pagination.pageCurrent": "Pagina {{page}} (curentă)"
"pagination.pageCurrent": "Pagina {{page}} (curentă)",
"pagination.pageLabel": "Pagină"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Показать объявления",
"top": "Вверх",
"reply_count": "Количество ответов",
"pagination.pageCurrent": "Страница {{page}} (текущая)"
"pagination.pageCurrent": "Страница {{page}} (текущая)",
"pagination.pageLabel": "Страница"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Shfaq Njoftimet",
"top": "Lart",
"reply_count": "Numri i përgjigjeve",
"pagination.pageCurrent": "Faqja {{page}} (aktuale)"
"pagination.pageCurrent": "Faqja {{page}} (aktuale)",
"pagination.pageLabel": "Faqe"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Visa Meddelanden",
"top": "Topp",
"reply_count": "Antal svar",
"pagination.pageCurrent": "Sida {{page}} (nuvarande)"
"pagination.pageCurrent": "Sida {{page}} (nuvarande)",
"pagination.pageLabel": "Sida"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "ప్రకటనలు చూపించు",
"top": "పై",
"reply_count": "సమాధానాల సంఖ్య",
"pagination.pageCurrent": "పేజీ {{page}} (ప్రస్తుత)"
"pagination.pageCurrent": "పేజీ {{page}} (ప్రస్తుత)",
"pagination.pageLabel": "పేజీ"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "แสดงประกาศ",
"top": "บน",
"reply_count": "จำนวนการตอบกลับ",
"pagination.pageCurrent": "หน้า {{page}} (ปัจจุบัน)"
"pagination.pageCurrent": "หน้า {{page}} (ปัจจุบัน)",
"pagination.pageLabel": "หน้า"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Duyuruları Göster",
"top": "Üst",
"reply_count": "Yanıt sayısı",
"pagination.pageCurrent": "Sayfa {{page}} (mevcut)"
"pagination.pageCurrent": "Sayfa {{page}} (mevcut)",
"pagination.pageLabel": "Sayfa"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Показати оголошення",
"top": "Вгору",
"reply_count": "Кількість відповідей",
"pagination.pageCurrent": "Сторінка {{page}} (поточна)"
"pagination.pageCurrent": "Сторінка {{page}} (поточна)",
"pagination.pageLabel": "Сторінка"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "اعلانات دکھائیں",
"top": "اوپر",
"reply_count": "جوابوں کی تعداد",
"pagination.pageCurrent": "صفحہ {{page}} (موجودہ)"
"pagination.pageCurrent": "صفحہ {{page}} (موجودہ)",
"pagination.pageLabel": "صفحہ"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "Hiển thị Thông báo",
"top": "Trên",
"reply_count": "Số lượng phản hồi",
"pagination.pageCurrent": "Trang {{page}} (hiện tại)"
"pagination.pageCurrent": "Trang {{page}} (hiện tại)",
"pagination.pageLabel": "Trang"
}
+2 -1
View File
@@ -281,5 +281,6 @@
"show_blotter": "显示公告",
"top": "顶部",
"reply_count": "回复数",
"pagination.pageCurrent": "第{{page}}页(当前)"
"pagination.pageCurrent": "第{{page}}页(当前)",
"pagination.pageLabel": "页"
}
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAccountComment, useComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
import { isAllView, isCatalogView, isModView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { usePostPageNumber } from '../../hooks/use-post-page-number';
import { useDirectories } from '../../hooks/use-directories';
import { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils';
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
@@ -457,7 +458,11 @@ export const MobileBoardButtons = () => {
export const PostPageStats = () => {
const { t } = useTranslation();
const params = useParams();
const location = useLocation();
const commentCid = params?.commentCid as string | undefined;
const resolvedAddress = useResolvedSubplebbitAddress();
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress;
const comment = useComment({ commentCid });
const postCid = comment?.parentCid ?? commentCid;
@@ -466,6 +471,13 @@ export const PostPageStats = () => {
const { closed, pinned, replyCount } = post || {};
const linkCount = useCountLinksInReplies(post);
const isThreadView = isPostPageView(location.pathname, params);
const pageNumber = usePostPageNumber({
subplebbitAddress,
postCid,
enabled: isThreadView,
});
const displayReplyCount = replyCount !== undefined ? replyCount.toString() : '?';
const replyCountTooltip = replyCount !== undefined ? capitalize(t('replies')) : t('loading');
@@ -474,6 +486,12 @@ export const PostPageStats = () => {
{pinned && `${capitalize(t('sticky'))} / `}
{closed && `${capitalize(t('closed'))} / `}
<Tooltip content={replyCountTooltip}>{displayReplyCount}</Tooltip> / <Tooltip content={capitalize(t('links'))}>{linkCount?.toString()}</Tooltip>
{isThreadView && (
<>
{' '}
/ <Tooltip content={pageNumber != null ? t('pagination.pageLabel') : t('loading')}>{pageNumber?.toString() ?? '?'}</Tooltip>
</>
)}
</span>
);
};
+55
View File
@@ -0,0 +1,55 @@
import { useMemo } from 'react';
import { useFeed } from '@plebbit/plebbit-react-hooks';
import useFeedsStore from '@plebbit/plebbit-react-hooks/dist/stores/feeds';
import { useDirectoryByAddress } from './use-directories';
import { useBoardFeedPageSize } from './use-board-feed-page-size';
import { findPostPageInFeed, findPostPageInLoadedBoardFeeds, type FeedsOptionsLike, type LoadedFeedsLike } from '../lib/utils/post-page-resolution';
export interface UsePostPageNumberOptions {
subplebbitAddress: string | undefined;
postCid: string | undefined;
/** When false, page segment is excluded (e.g. pending-post view). When true, resolve and show page. */
enabled?: boolean;
}
/**
* Resolve the board pagination page (1-based) for a post in a thread view.
* Uses cache-first lookup from feeds store, then preloads via useFeed if miss.
* Preload depth is capped to board pagination window (10 pages via paginationFeedPostsPerPage).
*
* @returns 1-based page number, or undefined when unresolved (render as "?")
*/
export function usePostPageNumber({ subplebbitAddress, postCid, enabled = true }: UsePostPageNumberOptions): number | undefined {
const community = useDirectoryByAddress(subplebbitAddress);
const { guiPostsPerPage, paginationFeedPostsPerPage } = useBoardFeedPageSize(community);
const canResolve = Boolean(enabled && subplebbitAddress && postCid && guiPostsPerPage > 0);
// Cache-first: selector returns only computed page to minimize rerenders
const cachedPage = useFeedsStore((state) => {
if (!canResolve) return undefined;
return findPostPageInLoadedBoardFeeds(state.feedsOptions as FeedsOptionsLike, state.loadedFeeds as LoadedFeedsLike, subplebbitAddress!, postCid!, guiPostsPerPage);
});
// Preload when cache miss and enabled (10 GUI pages)
const preloadOptions = useMemo(
() =>
canResolve
? {
subplebbitAddresses: [subplebbitAddress!],
sortType: 'active' as const,
postsPerPage: paginationFeedPostsPerPage,
}
: undefined,
[canResolve, subplebbitAddress, paginationFeedPostsPerPage],
);
const { feed: preloadFeed } = useFeed(preloadOptions);
const preloadedPage = useMemo(() => {
if (!canResolve || !preloadFeed?.length) return undefined;
return findPostPageInFeed(preloadFeed, postCid!, guiPostsPerPage);
}, [canResolve, postCid, preloadFeed, guiPostsPerPage]);
return cachedPage ?? preloadedPage;
}
+96
View File
@@ -0,0 +1,96 @@
/**
* Post page resolution utilities.
* Page semantics are board pagination pages (not catalog ordering, not fetch-chunk index).
*/
/** Minimal Comment shape with cid */
export interface CommentWithCid {
cid?: string;
[key: string]: unknown;
}
/** Minimal FeedOptions shape for board-feed filtering */
export interface FeedOptionsLike {
subplebbitAddresses: string[];
sortType: string;
postsPerPage?: number;
filter?: unknown;
newerThan?: number;
modQueue?: unknown;
accountComments?: unknown;
}
/** FeedsOptions-like map */
export type FeedsOptionsLike = Record<string, FeedOptionsLike>;
/** Loaded feeds map: feedName -> Comment[] */
export type LoadedFeedsLike = Record<string, CommentWithCid[]>;
/**
* Find the board pagination page (1-based) containing postCid in a feed.
* Pure index math: page = floor(index / guiPostsPerPage) + 1.
*
* @param feed - Array of comments (posts) in feed order
* @param postCid - CID of the post (OP) to locate
* @param guiPostsPerPage - Posts per GUI page
* @returns 1-based page number, or undefined if post not found
*/
export function findPostPageInFeed(feed: CommentWithCid[], postCid: string, guiPostsPerPage: number): number | undefined {
if (!postCid || guiPostsPerPage <= 0) return undefined;
const index = feed.findIndex((c) => c.cid === postCid);
if (index < 0) return undefined;
return Math.floor(index / guiPostsPerPage) + 1;
}
/**
* Strict board-feed filter criteria.
* A feed is a "board feed" iff:
* - sortType === 'active'
* - single-sub feed (subplebbitAddresses.length === 1)
* - no filter, no newerThan, no modQueue, no accountComments
*/
export function isBoardFeedOptions(opts: FeedOptionsLike, subplebbitAddress: string): boolean {
return (
opts.sortType === 'active' &&
opts.subplebbitAddresses?.length === 1 &&
opts.subplebbitAddresses[0] === subplebbitAddress &&
!opts.filter &&
opts.newerThan == null &&
!opts.modQueue &&
!opts.accountComments
);
}
/**
* Find the board pagination page for postCid by searching loaded board feeds.
* Only considers feeds matching strict board-feed criteria.
*
* @param feedsOptions - Feeds store feedsOptions
* @param loadedFeeds - Feeds store loadedFeeds
* @param subplebbitAddress - Board subplebbit address
* @param postCid - CID of the post (OP) to locate
* @param guiPostsPerPage - Posts per GUI page
* @returns 1-based page number, or undefined if not found in any matching feed
*/
export function findPostPageInLoadedBoardFeeds(
feedsOptions: FeedsOptionsLike,
loadedFeeds: LoadedFeedsLike,
subplebbitAddress: string,
postCid: string,
guiPostsPerPage: number,
): number | undefined {
if (!subplebbitAddress || !postCid || guiPostsPerPage <= 0) return undefined;
for (const feedName of Object.keys(feedsOptions)) {
const opts = feedsOptions[feedName];
if (!opts || !isBoardFeedOptions(opts, subplebbitAddress)) continue;
const feed = loadedFeeds[feedName];
if (!feed || !Array.isArray(feed)) continue;
const page = findPostPageInFeed(feed, postCid, guiPostsPerPage);
if (page !== undefined) return page;
}
return undefined;
}