| 1 | import { BellOff, BellRing } from "lucide-react"; |
| 2 | import { useEffect, useState } from "react"; |
| 3 | |
| 4 | import { Button } from "../ui/button"; |
| 5 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu"; |
| 6 | import { Hint } from "../ui/hint"; |
| 7 | import { setPresence, useOwnPresence } from "../../lib/notify-client"; |
| 8 | import { PAUSE_FOR, dndOn, pauseUntil, untilLabel } from "../../lib/presence"; |
| 9 | |
| 10 | /** |
| 11 | * Do not disturb from the chat sidebar's header: one bell-off button. Off, |
| 12 | * it opens the choices (30 minutes, an hour, two, tomorrow at 9); on, it |
| 13 | * is lit and the menu ends it. The same Do not disturb as the avatar |
| 14 | * menu's Pause notifications and the settings page: your presence |
| 15 | * (`dnd_until`), which silences sounds, pop-ups and pushes alike. |
| 16 | */ |
| 17 | export function DndMenu({ className }: { className?: string }) { |
| 18 | const me = useOwnPresence(); |
| 19 | const [now, setNow] = useState(() => Date.now()); |
| 20 | // It ends on its own: the button follows, a minute at a time. |
| 21 | useEffect(() => { |
| 22 | const timer = setInterval(() => setNow(Date.now()), 60_000); |
| 23 | return () => clearInterval(timer); |
| 24 | }, []); |
| 25 | const on = dndOn(me, now); |
| 26 | const until = on && me?.dnd_until ? untilLabel(me.dnd_until, new Date(now)) : null; |
| 27 | return ( |
| 28 | <DropdownMenu> |
| 29 | <Hint label={on ? `Do not disturb, ${until}` : "Do not disturb"}> |
| 30 | <DropdownMenuTrigger asChild> |
| 31 | <Button |
| 32 | variant="ghost" |
| 33 | size="icon-xs" |
| 34 | aria-label={on ? `Do not disturb is on, ${until}` : "Do not disturb"} |
| 35 | aria-pressed={on} |
| 36 | className={`${on ? "text-warn hover:text-warn aria-pressed:text-warn" : "text-faint"} ${className ?? ""}`} |
| 37 | > |
| 38 | <BellOff size={15} /> |
| 39 | </Button> |
| 40 | </DropdownMenuTrigger> |
| 41 | </Hint> |
| 42 | <DropdownMenuContent align="end" className="min-w-52"> |
| 43 | {on ? ( |
| 44 | <DropdownMenuItem |
| 45 | className="gap-2.5 px-2.5" |
| 46 | onSelect={() => { |
| 47 | void setPresence({ dnd_until: null }, { dnd_until: null }); |
| 48 | }} |
| 49 | > |
| 50 | <BellRing /> |
| 51 | <span className="grow">Resume notifications</span> |
| 52 | {until && <span className="text-xs text-faint">{until}</span>} |
| 53 | </DropdownMenuItem> |
| 54 | ) : ( |
| 55 | PAUSE_FOR.map((choice) => ( |
| 56 | <DropdownMenuItem |
| 57 | key={choice.key} |
| 58 | className="px-2.5" |
| 59 | onSelect={() => { |
| 60 | const at = pauseUntil(choice.key, new Date()); |
| 61 | void setPresence({ dnd_until: at }, { dnd_until: at }); |
| 62 | }} |
| 63 | > |
| 64 | {choice.label} |
| 65 | </DropdownMenuItem> |
| 66 | )) |
| 67 | )} |
| 68 | <DropdownMenuSeparator /> |
| 69 | <p className="max-w-56 px-2.5 py-1 text-xs leading-snug text-faint"> |
| 70 | {on ? "No sounds, pop-ups or browser notifications until then. Counts still move." : "Silences sounds, pop-ups and browser notifications. Counts still move."} |
| 71 | </p> |
| 72 | </DropdownMenuContent> |
| 73 | </DropdownMenu> |
| 74 | ); |
| 75 | } |