"use client"; /** * The announcement surface: a modal on desktop, a drawer on mobile. * * Shown at most once per user, and that guarantee lives on the server — the * dismissal is a row, not local storage — so it survives a new device, a * cleared browser, and two tabs racing. * * It appears on load and on the realtime nudge, but never over a signed-out * page: an announcement is addressed to a person, and there is nobody to * address until they're signed in. */ import { useCallback, useEffect, useState } from "react"; import Image from "next/image"; import { useAuth } from "@/components/auth/auth-provider"; import { useNotifications } from "@/components/notifications/notifications-provider"; import { ResponsiveDialog } from "@/components/chat/responsive-dialog"; import { MessageText } from "@/components/chat/message-text"; import { Button } from "@/components/ui/button"; import { broadcasts, type Broadcast } from "@/lib/broadcasts"; export function BroadcastModal() { const { user } = useAuth(); const { lastBroadcastAt } = useNotifications(); const [item, setItem] = useState(null); const [dismissing, setDismissing] = useState(false); const load = useCallback(() => { if (!user?.id) return; broadcasts .pending() .then(setItem) .catch(() => { /* announcements are never worth an error surface */ }); }, [user?.id]); // On sign-in, and again whenever the realtime nudge fires. lastBroadcastAt // changes on each publish, so this refetches without the event carrying any // content of its own. useEffect(() => { load(); }, [load, lastBroadcastAt]); const dismiss = useCallback(async () => { if (!item || dismissing) return; setDismissing(true); // Close first: the dismissal is idempotent and the server already knows // what this user has seen, so a failed write costs at most one repeat // rather than a modal that won't go away. const id = item.id; setItem(null); setDismissing(false); try { await broadcasts.markSeen(id); } catch { /* it will simply be offered again */ } }, [item, dismissing]); // Guard on the shape, not just presence: a malformed payload must not put // an empty modal over every page in the app. if (!item?.id || !item.title) return null; return ( {item.cta_url && item.cta_label ? ( <> {item.cta_label} ) : ( )} } >
{item.banner_url && (
)}
); }