refactor: enhance announcements functionality to include expired announcements in hooks and components

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-07-16 20:03:09 -07:00
parent 7bc02ce55b
commit 7af4bc0b49
5 changed files with 55 additions and 32 deletions

View file

@ -10,7 +10,7 @@ import { useAnnouncements } from "@/hooks/use-announcements";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export default function AnnouncementsPage() { 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 // Auto-mark all visible announcements as read when the page is opened
useEffect(() => { useEffect(() => {

View file

@ -11,7 +11,7 @@ import { useAnnouncements } from "@/hooks/use-announcements";
export function AnnouncementsDialog() { export function AnnouncementsDialog() {
const [open, setOpen] = useAtom(announcementsDialogAtom); 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 // Auto-mark all visible announcements as read when the dialog opens
useEffect(() => { useEffect(() => {

View file

@ -11,6 +11,7 @@ import {
} from "@/lib/announcements/announcements-storage"; } from "@/lib/announcements/announcements-storage";
import { import {
getActiveAnnouncements, getActiveAnnouncements,
getPublishedAnnouncements,
msUntilNextTransition, msUntilNextTransition,
} from "@/lib/announcements/announcements-utils"; } from "@/lib/announcements/announcements-utils";
import { isAuthenticated } from "@/lib/auth-utils"; import { isAuthenticated } from "@/lib/auth-utils";
@ -56,10 +57,16 @@ export interface AnnouncementWithState extends Announcement {
interface UseAnnouncementsOptions { interface UseAnnouncementsOptions {
/** Filter by category */ /** Filter by category */
category?: AnnouncementCategory; 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 = {}) { export function useAnnouncements(options: UseAnnouncementsOptions = {}) {
const { category } = options; const { category, includeExpired = false } = options;
// Subscribe to state changes (re-renders when localStorage state is bumped) // Subscribe to state changes (re-renders when localStorage state is bumped)
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
@ -71,12 +78,13 @@ export function useAnnouncements(options: UseAnnouncementsOptions = {}) {
const enriched: AnnouncementWithState[] = useMemo(() => { const enriched: AnnouncementWithState[] = useMemo(() => {
const authed = isAuthenticated(); const authed = isAuthenticated();
const now = new Date(); const now = new Date();
let items: AnnouncementWithState[] = getActiveAnnouncements(announcements, authed, now).map( const visible = includeExpired
(a) => ({ ? getPublishedAnnouncements(announcements, authed, now)
...a, : getActiveAnnouncements(announcements, authed, now);
isRead: isAnnouncementRead(a.id), let items: AnnouncementWithState[] = visible.map((a) => ({
}) ...a,
); isRead: isAnnouncementRead(a.id),
}));
if (category) { if (category) {
items = items.filter((a) => a.category === category); items = items.filter((a) => a.category === category);
@ -86,7 +94,7 @@ export function useAnnouncements(options: UseAnnouncementsOptions = {}) {
return items; return items;
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 // Schedule a re-render when the next announcement starts or expires
useEffect(() => { useEffect(() => {

View file

@ -54,28 +54,17 @@ export const announcements: Announcement[] = [
url: "/changelog", url: "/changelog",
}, },
}, },
{ // {
id: "announcement-1", // id: "announcement-1",
title: "Introducing What's New", // title: "Introducing What's New",
description: "All major product updates will be posted here.", // description: "All major product updates will be posted here.",
category: "feature", // category: "feature",
date: "2026-02-17T00:00:00Z", // date: "2026-02-17T00:00:00Z",
startTime: "2026-02-17T00:00:00Z", // startTime: "2026-02-17T00:00:00Z",
endTime: "2026-02-20T00:00:00Z", // endTime: "2026-02-20T00:00:00Z",
audience: "all", // audience: "all",
isImportant: false, // 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: "2026-02-10-podcast-improvements", // id: "2026-02-10-podcast-improvements",
// title: "Podcast Generation Improvements", // title: "Podcast Generation Improvements",

View file

@ -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 * Filter announcements to only those that are currently active and
* targeted at the given audience. * targeted at the given audience.