diff --git a/frontend/app/components/Feed-comp/FeedList.tsx b/frontend/app/components/Feed-comp/FeedList.tsx index e9569e4..29d797d 100644 --- a/frontend/app/components/Feed-comp/FeedList.tsx +++ b/frontend/app/components/Feed-comp/FeedList.tsx @@ -1,6 +1,12 @@ +"use client"; + +import { useMemo, useRef } from "react"; import FeedItem from "./FeedItem"; -export type FeedActivityType = "code" | "discussion" | "milestone"; +export type FeedActivityType = + | "code" + | "discussion" + | "milestone"; export type FeedActivityItem = { id: string; @@ -22,37 +28,169 @@ type FeedListProps = { onLoadMore: () => void; }; -export default function FeedList({ items, totalCount, onLoadMore }: FeedListProps) { - const groupedItems = items.reduce>((acc, item) => { - if (!acc[item.group]) { - acc[item.group] = []; - } - acc[item.group].push(item); - return acc; - }, {}); +type PaginationSnapshot = { + totalItems: number; + visibleItems: number; + pageVersion: number; +}; + +type PaginationDiagnostics = { + reconciliationCount: number; + boundaryAdjustments: number; + datasetMutations: number; +}; + +function createPaginationSnapshot( + items: FeedActivityItem[], + totalCount: number +): PaginationSnapshot { + return { + totalItems: totalCount, + visibleItems: items.length, + pageVersion: Date.now(), + }; +} + +function detectDatasetMutation( + current: PaginationSnapshot, + previous: PaginationSnapshot | null +) { + if (!previous) { + return false; + } + + return ( + current.totalItems !== previous.totalItems || + current.visibleItems !== previous.visibleItems + ); +} + +function normalizePaginationBoundary( + visibleItems: number, + totalItems: number +) { + return Math.min( + visibleItems, + totalItems + ); +} + +function reconcilePaginationState( + snapshot: PaginationSnapshot +) { + return { + ...snapshot, + visibleItems: + normalizePaginationBoundary( + snapshot.visibleItems, + snapshot.totalItems + ), + }; +} + +function buildPaginationDiagnostics(): PaginationDiagnostics { + return { + reconciliationCount: 0, + boundaryAdjustments: 0, + datasetMutations: 0, + }; +} + +export default function FeedList({ + items, + totalCount, + onLoadMore, +}: FeedListProps) { + const previousSnapshotRef = + useRef( + null + ); + + const diagnosticsRef = + useRef( + buildPaginationDiagnostics() + ); + + const paginationSnapshot = + useMemo( + () => + reconcilePaginationState( + createPaginationSnapshot( + items, + totalCount + ) + ), + [items, totalCount] + ); + + const datasetMutated = + detectDatasetMutation( + paginationSnapshot, + previousSnapshotRef.current + ); + + if (datasetMutated) { + diagnosticsRef.current.datasetMutations += 1; + diagnosticsRef.current.reconciliationCount += 1; + } + + previousSnapshotRef.current = + paginationSnapshot; + + const groupedItems = useMemo( + () => + items.reduce< + Record< + string, + FeedActivityItem[] + > + >((acc, item) => { + if (!acc[item.group]) { + acc[item.group] = []; + } + + acc[item.group].push(item); + + return acc; + }, {}), + [items] + ); return ( <> - {Object.entries(groupedItems).map(([group, groupItems]) => ( -
-
- {group} -
-
- - {groupItems.map((item) => ( - - ))} -
- ))} + {Object.entries(groupedItems).map( + ([group, groupItems]) => ( +
+
+ + {group} + + +
+
+ + {groupItems.map((item) => ( + + ))} +
+ ) + )} {items.length === 0 && (
- No activity matches the current filters. + No activity matches the + current filters.
)} - {items.length < totalCount && ( + {items.length < + totalCount && (
)} + +
); -} +} \ No newline at end of file