import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { CARD_PAGE_SIZES, parseCardSize } from "./channelLayout"; import { readAccount, writeAccount } from "./storage"; // Client-side pagination for the channel manager's card layout, owned once instead of twice. Both // manager tabs page an in-memory list the same way, and keeping two copies meant every fix landed // twice (and the day one copy was missed, the tabs would page differently). The caller keeps only // what genuinely differs: which rows, which storage key, and when to jump back to page one. // // The page size persists per account; the page itself is transient. `enabled` is the card layout // flag — in table mode DataTable does its own paging, so this does no work and holds still. export function useCardPager( rows: T[], sizeKey: string, enabled: boolean, /** Follow a row the caller just moved: each new `seq` turns to the page holding the row `match` * finds — the same contract as `DataTable`'s `reveal`, for the card paint of the same list. */ reveal?: { seq: number; match: (row: T) => boolean } | null, /** The size picker's name — what the cards ARE (the channel manager's are "cards"). */ sizeLabel?: string, /** Which list `rows` are: another one (a folder, a search, a sort) starts at page one. */ scope?: string, ) { const { t } = useTranslation(); const [size, setSize] = useState(() => parseCardSize(readAccount(sizeKey, null))); const [page, setPage] = useState(0); const [scopeSeen, setScopeSeen] = useState(scope); if (scope !== scopeSeen) { setScopeSeen(scope); setPage(0); } // During render against the last seq seen, as in DataTable (seeded so a mount never jumps). const [revealSeen, setRevealSeen] = useState(reveal?.seq); if (reveal && reveal.seq !== revealSeen) { setRevealSeen(reveal.seq); const at = rows.findIndex(reveal.match); if (enabled && at >= 0 && size > 0) setPage(Math.floor(at / size)); } const totalPages = size <= 0 ? 1 : Math.max(1, Math.ceil(rows.length / size)); const safePage = Math.min(page, totalPages - 1); // A list that shrank under you (an unsubscribe, a refetch) leaves `page` out of range: `safePage` // above only HIDES that, so commit it or it springs back the moment the list grows again. Only // fires when the page you are on stopped existing, so it never costs you your place otherwise. // // Adjusted DURING RENDER, not from an effect: React re-runs this component immediately and throws // the first pass away, so nothing is committed with an out-of-range page and there is no second // paint. The condition is what makes that safe — the very write that triggers it also falsifies it. if (enabled && page > totalPages - 1) setPage(totalPages - 1); const paged = !enabled || size <= 0 ? rows : rows.slice(safePage * size, safePage * size + size); return { paged, /** Spread onto . */ pagerProps: { page: safePage, size, total: rows.length, pageSizeOptions: CARD_PAGE_SIZES, onPageChange: setPage, onSizeChange: (s: number) => { setSize(s); writeAccount(sizeKey, s); setPage(0); }, sizeLabel: sizeLabel ?? t("channels.cardsPerPage"), }, /** Call when the caller's own filters change — a different list starts at page one. */ resetPage: useCallback(() => setPage(0), []), }; }