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() {
const { announcements, markAllRead } = useAnnouncements();
const { announcements, markAllRead } = useAnnouncements({ includeExpired: true });
// Auto-mark all visible announcements as read when the page is opened
useEffect(() => {

View file

@ -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(() => {

View file

@ -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(() => {

View file

@ -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",

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
* targeted at the given audience.