SurfSense/surfsense_web/hooks/use-announcements.ts
2026-02-12 16:12:45 -08:00

119 lines
3.3 KiB
TypeScript

"use client";
import { useCallback, useMemo, useSyncExternalStore } from "react";
import type { Announcement, AnnouncementCategory } from "@/contracts/types/announcement.types";
import { announcements } from "@/lib/announcements/announcements-data";
import {
dismissAnnouncement,
getAnnouncementState,
isAnnouncementDismissed,
isAnnouncementRead,
markAllAnnouncementsRead,
markAnnouncementRead,
} from "@/lib/announcements/announcements-storage";
// ---------------------------------------------------------------------------
// External-store plumbing so React re-renders when localStorage changes
// ---------------------------------------------------------------------------
let stateVersion = 0;
const listeners = new Set<() => void>();
function subscribe(callback: () => void) {
listeners.add(callback);
return () => listeners.delete(callback);
}
function getSnapshot() {
return stateVersion;
}
function getServerSnapshot() {
return 0;
}
/** Bump the version so useSyncExternalStore triggers a re-render */
function notify() {
stateVersion++;
for (const listener of listeners) listener();
}
// ---------------------------------------------------------------------------
// Enriched announcement with read/dismissed state
// ---------------------------------------------------------------------------
export interface AnnouncementWithState extends Announcement {
isRead: boolean;
isDismissed: boolean;
}
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
interface UseAnnouncementsOptions {
/** Filter by category */
category?: AnnouncementCategory;
/** If true, include dismissed announcements (default: false) */
includeDismissed?: boolean;
}
export function useAnnouncements(options: UseAnnouncementsOptions = {}) {
const { category, includeDismissed = false } = options;
// Subscribe to state changes (re-renders when localStorage state is bumped)
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
const enriched: AnnouncementWithState[] = useMemo(() => {
let items = announcements.map((a) => ({
...a,
isRead: isAnnouncementRead(a.id),
isDismissed: isAnnouncementDismissed(a.id),
}));
if (category) {
items = items.filter((a) => a.category === category);
}
if (!includeDismissed) {
items = items.filter((a) => !a.isDismissed);
}
// Sort by date descending (newest first)
items.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return items;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [category, includeDismissed, stateVersion]);
const unreadCount = useMemo(
() => enriched.filter((a) => !a.isRead && !a.isDismissed).length,
[enriched]
);
const handleMarkRead = useCallback((id: string) => {
markAnnouncementRead(id);
notify();
}, []);
const handleMarkAllRead = useCallback(() => {
const state = getAnnouncementState();
const unreadIds = announcements.filter((a) => !state.readIds.includes(a.id)).map((a) => a.id);
markAllAnnouncementsRead(unreadIds);
notify();
}, []);
const handleDismiss = useCallback((id: string) => {
dismissAnnouncement(id);
markAnnouncementRead(id);
notify();
}, []);
return {
announcements: enriched,
unreadCount,
markRead: handleMarkRead,
markAllRead: handleMarkAllRead,
dismiss: handleDismiss,
};
}