import type { Feed } from '../../db/schema'; import type { CategoryState, FeedWithCount } from './types'; /** * Single source of truth for TUI unread-count math. * * Two consumers ride on these helpers: * - `appReducer` applies *deltas* on UPDATE_ARTICLE (via apply* helpers). * - `useDataLoader` *recomputes* totals from-scratch on full reload (via build* helpers). * * Both paths must agree, otherwise the TUI silently drifts (Read/Unread shows * the wrong total). Putting the formula in one place is what guarantees that. */ export function clampCount(count: number): number { return Math.max(0, count); } export function applyDeltaToFeeds(feeds: FeedWithCount[], feedId: number, delta: number): FeedWithCount[] { if (delta === 0) return feeds; return feeds.map((feed) => feed.id === feedId ? { ...feed, unreadCount: clampCount(feed.unreadCount + delta) } : feed, ); } export function applyDeltaToCategories(categories: CategoryState[], feedId: number, delta: number): CategoryState[] { if (delta === 0) return categories; return categories.map((category) => { if (!category.feeds.some((feed) => feed.id === feedId)) { return category; } return { ...category, totalUnread: clampCount(category.totalUnread + delta), feeds: category.feeds.map((feed) => feed.id === feedId ? { ...feed, unreadCount: clampCount(feed.unreadCount + delta) } : feed, ), }; }); } /** * Join feeds with their per-feed unread stats. Feeds without a stat default to 0. */ export function buildFeedsWithCounts( feeds: Feed[], statsByFeed: Array<{ feedId: number; unread: number }>, ): FeedWithCount[] { const lookup = new Map(statsByFeed.map((s) => [s.feedId, s.unread])); return feeds.map((feed) => ({ ...feed, unreadCount: lookup.get(feed.id) ?? 0 })); } /** * Group feeds by category, deriving each category's total from its feeds' * unread counts. Uncategorized feeds collect into a trailing empty-name bucket * (only emitted when at least one uncategorized feed exists). */ export function buildCategoryStates(feeds: FeedWithCount[]): CategoryState[] { const states: CategoryState[] = []; const byName = new Map(); const uncategorized: CategoryState = { name: '', collapsed: false, feeds: [], totalUnread: 0 }; for (const feed of feeds) { if (!feed.category) { uncategorized.feeds.push(feed); uncategorized.totalUnread += feed.unreadCount; continue; } let category = byName.get(feed.category); if (!category) { category = { name: feed.category, collapsed: false, feeds: [], totalUnread: 0 }; byName.set(feed.category, category); states.push(category); } category.feeds.push(feed); category.totalUnread += feed.unreadCount; } if (uncategorized.feeds.length > 0) states.push(uncategorized); return states; }