From 7af4bc0b49385ae78d32fc1d9d50fbe682d958c4 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Thu, 16 Jul 2026 20:03:09 -0700 Subject: [PATCH] refactor: enhance announcements functionality to include expired announcements in hooks and components --- .../app/(home)/announcements/page.tsx | 2 +- .../announcements/AnnouncementsDialog.tsx | 2 +- surfsense_web/hooks/use-announcements.ts | 24 +++++++++----- .../lib/announcements/announcements-data.ts | 33 +++++++------------ .../lib/announcements/announcements-utils.ts | 26 +++++++++++++++ 5 files changed, 55 insertions(+), 32 deletions(-) diff --git a/surfsense_web/app/(home)/announcements/page.tsx b/surfsense_web/app/(home)/announcements/page.tsx index f287e43d1..6d4fb48ed 100644 --- a/surfsense_web/app/(home)/announcements/page.tsx +++ b/surfsense_web/app/(home)/announcements/page.tsx @@ -10,7 +10,7 @@ import { useAnnouncements } from "@/hooks/use-announcements"; // --------------------------------------------------------------------------- export default function AnnouncementsPage() { - const { announcements, markAllRead } = useAnnouncements(); + const { announcements, markAllRead } = useAnnouncements({ includeExpired: true }); // Auto-mark all visible announcements as read when the page is opened useEffect(() => { diff --git a/surfsense_web/components/announcements/AnnouncementsDialog.tsx b/surfsense_web/components/announcements/AnnouncementsDialog.tsx index 097103e3d..84908e2aa 100644 --- a/surfsense_web/components/announcements/AnnouncementsDialog.tsx +++ b/surfsense_web/components/announcements/AnnouncementsDialog.tsx @@ -11,7 +11,7 @@ import { useAnnouncements } from "@/hooks/use-announcements"; export function AnnouncementsDialog() { const [open, setOpen] = useAtom(announcementsDialogAtom); - const { announcements, markAllRead } = useAnnouncements(); + const { announcements, markAllRead } = useAnnouncements({ includeExpired: true }); // Auto-mark all visible announcements as read when the dialog opens useEffect(() => { diff --git a/surfsense_web/hooks/use-announcements.ts b/surfsense_web/hooks/use-announcements.ts index b046360d3..49452f31c 100644 --- a/surfsense_web/hooks/use-announcements.ts +++ b/surfsense_web/hooks/use-announcements.ts @@ -11,6 +11,7 @@ import { } from "@/lib/announcements/announcements-storage"; import { getActiveAnnouncements, + getPublishedAnnouncements, msUntilNextTransition, } from "@/lib/announcements/announcements-utils"; import { isAuthenticated } from "@/lib/auth-utils"; @@ -56,10 +57,16 @@ export interface AnnouncementWithState extends Announcement { interface UseAnnouncementsOptions { /** Filter by category */ category?: AnnouncementCategory; + /** + * Include announcements whose visibility window has expired (archive + * views). Defaults to false so spotlights, toasts, and unread badges + * only consider currently-active announcements. + */ + includeExpired?: boolean; } export function useAnnouncements(options: UseAnnouncementsOptions = {}) { - const { category } = options; + const { category, includeExpired = false } = options; // Subscribe to state changes (re-renders when localStorage state is bumped) useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); @@ -71,12 +78,13 @@ export function useAnnouncements(options: UseAnnouncementsOptions = {}) { const enriched: AnnouncementWithState[] = useMemo(() => { const authed = isAuthenticated(); const now = new Date(); - let items: AnnouncementWithState[] = getActiveAnnouncements(announcements, authed, now).map( - (a) => ({ - ...a, - isRead: isAnnouncementRead(a.id), - }) - ); + const visible = includeExpired + ? getPublishedAnnouncements(announcements, authed, now) + : getActiveAnnouncements(announcements, authed, now); + let items: AnnouncementWithState[] = visible.map((a) => ({ + ...a, + isRead: isAnnouncementRead(a.id), + })); if (category) { items = items.filter((a) => a.category === category); @@ -86,7 +94,7 @@ export function useAnnouncements(options: UseAnnouncementsOptions = {}) { return items; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [category, stateVersion, tick]); + }, [category, includeExpired, stateVersion, tick]); // Schedule a re-render when the next announcement starts or expires useEffect(() => { diff --git a/surfsense_web/lib/announcements/announcements-data.ts b/surfsense_web/lib/announcements/announcements-data.ts index f916ed61b..75194e0a4 100644 --- a/surfsense_web/lib/announcements/announcements-data.ts +++ b/surfsense_web/lib/announcements/announcements-data.ts @@ -54,28 +54,17 @@ export const announcements: Announcement[] = [ url: "/changelog", }, }, - { - id: "announcement-1", - title: "Introducing What's New", - description: "All major product updates will be posted here.", - category: "feature", - date: "2026-02-17T00:00:00Z", - startTime: "2026-02-17T00:00:00Z", - endTime: "2026-02-20T00:00:00Z", - audience: "all", - isImportant: false, - }, - { - id: "announcement-6", - title: "Past Test Announcement", - description: "This should be seen by nobody, because it's in the past.", - category: "maintenance", - date: "2026-02-17T00:00:00Z", - startTime: "2026-02-15T23:23:00Z", - endTime: "2026-02-16T00:00:00Z", - audience: "users", - isImportant: true, - }, + // { + // id: "announcement-1", + // title: "Introducing What's New", + // description: "All major product updates will be posted here.", + // category: "feature", + // date: "2026-02-17T00:00:00Z", + // startTime: "2026-02-17T00:00:00Z", + // endTime: "2026-02-20T00:00:00Z", + // audience: "all", + // isImportant: false, + // }, // { // id: "2026-02-10-podcast-improvements", // title: "Podcast Generation Improvements", diff --git a/surfsense_web/lib/announcements/announcements-utils.ts b/surfsense_web/lib/announcements/announcements-utils.ts index fde2b2a5c..35bc0976d 100644 --- a/surfsense_web/lib/announcements/announcements-utils.ts +++ b/surfsense_web/lib/announcements/announcements-utils.ts @@ -37,6 +37,32 @@ export function announcementMatchesAudience( } } +/** + * Returns true when the announcement has been published (startTime has + * passed), regardless of whether its visibility window has expired. + * Used for archive views like the announcements page. + */ +export function isAnnouncementPublished(announcement: Announcement, now = new Date()): boolean { + const start = new Date(announcement.startTime).getTime(); + if (Number.isNaN(start)) return false; + return now.getTime() >= start; +} + +/** + * Filter announcements to all published ones (including expired) targeted + * at the given audience. Powers archive views; use getActiveAnnouncements + * for toasts, spotlights, and unread badges. + */ +export function getPublishedAnnouncements( + announcements: Announcement[], + isAuthenticated: boolean, + now = new Date() +): Announcement[] { + return announcements.filter( + (a) => isAnnouncementPublished(a, now) && announcementMatchesAudience(a, isAuthenticated) + ); +} + /** * Filter announcements to only those that are currently active and * targeted at the given audience.