From 8da9c41b85eecd24e0df924517b30c88233f3fe4 Mon Sep 17 00:00:00 2001 From: Tryston Date: Sun, 20 Sep 2026 12:55:07 -0700 Subject: [PATCH] Flash the tab title and show an ack dialog when the doorbell rings Keep cycling between the default title and "Someone rang the doorbell" until the tab is back in view or the on-page dialog is dismissed, so organizers notice a ring even if the doorbell tab is in the background. --- app/doorbell/page.tsx | 103 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/app/doorbell/page.tsx b/app/doorbell/page.tsx index 9c498b5..bed6a1c 100644 --- a/app/doorbell/page.tsx +++ b/app/doorbell/page.tsx @@ -7,7 +7,9 @@ import Link from "next/link" import { PotionBackground } from "../components/PotionBackground" import { ErrorBoundary } from "../components/ErrorBoundary" import { Button } from "../components/Button" +import { Modal } from "../components/Modal" import { supabaseClient } from "@/lib/supabaseClient" +import { siteConfig } from "../siteConfig" import eventsData from "../data/events.json" import type { LumaEvent } from "../services/luma" @@ -15,9 +17,12 @@ import type { LumaEvent } from "../services/luma" export default function Doorbell() { const [isRinging, setIsRinging] = useState(false) + const [isAwaitingAck, setIsAwaitingAck] = useState(false) const [ringCount, setRingCount] = useState(0) const channelRef = useRef(null) const lastRingIdRef = useRef(null) + const defaultTitleRef = useRef("") + const titleIntervalRef = useRef | null>(null) const nearestEvent = useMemo(() => { const events = eventsData as LumaEvent[] @@ -34,10 +39,43 @@ export default function Doorbell() { }, null) }, []) + const stopTitleCycle = useCallback(() => { + if (titleIntervalRef.current !== null) { + clearInterval(titleIntervalRef.current) + titleIntervalRef.current = null + } + + if (defaultTitleRef.current) { + document.title = defaultTitleRef.current + } + }, []) + + const acknowledgeRing = useCallback(() => { + setIsAwaitingAck(false) + stopTitleCycle() + }, [stopTitleCycle]) + + const startRingAlert = useCallback(() => { + if (!defaultTitleRef.current) { + defaultTitleRef.current = document.title || siteConfig.title + } + + setIsAwaitingAck(true) + stopTitleCycle() + + let showAlertTitle = true + document.title = RING_ALERT_TITLE + titleIntervalRef.current = setInterval(() => { + showAlertTitle = !showAlertTitle + document.title = showAlertTitle ? RING_ALERT_TITLE : defaultTitleRef.current + }, TITLE_CYCLE_MS) + }, [stopTitleCycle]) + const triggerLocalRing = useCallback(() => { playDoorbellSound() setIsRinging(true) - }, []) + startRingAlert() + }, [startRingAlert]) const broadcastRing = useCallback(async (ringId: string) => { if (!channelRef.current) { @@ -109,6 +147,39 @@ export default function Doorbell() { } }, [isRinging]) + useEffect(() => { + if (!isAwaitingAck) { + return + } + + const handleBackInView = () => { + if (document.visibilityState === "visible") { + stopTitleCycle() + } + } + + document.addEventListener("visibilitychange", handleBackInView) + window.addEventListener("focus", handleBackInView) + + return () => { + document.removeEventListener("visibilitychange", handleBackInView) + window.removeEventListener("focus", handleBackInView) + } + }, [isAwaitingAck, stopTitleCycle]) + + useEffect(() => { + return () => { + if (titleIntervalRef.current !== null) { + clearInterval(titleIntervalRef.current) + titleIntervalRef.current = null + } + + if (defaultTitleRef.current) { + document.title = defaultTitleRef.current + } + } + }, []) + return ( <> @@ -175,6 +246,15 @@ export default function Doorbell() { )} + + Someone rang the doorbell + Someone is at the door and needs to be let in. + + + + ) } @@ -310,12 +390,33 @@ const TermsLink = styled(Link)` } ` +const AlertHeading = styled.h2` + font-size: 1.5rem; + font-weight: 700; + margin: 0 2rem 1rem 0; + color: white; +` + +const AlertMessage = styled.p` + font-size: 1rem; + margin: 0 0 1.5rem 0; + color: rgba(255, 255, 255, 0.7); +` + +const AlertActions = styled.div` + display: flex; + justify-content: flex-end; +` + // Constants // type RingPayload = { ringId?: string } +const RING_ALERT_TITLE = "Someone rang the doorbell" +const TITLE_CYCLE_MS = 1000 + const createRingIdentifier = () => { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID()