Skip to content
75 linesCodeBlameRaw
1import { BellOff, BellRing } from "lucide-react";
2import { useEffect, useState } from "react";
3
4import { Button } from "../ui/button";
5import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu";
6import { Hint } from "../ui/hint";
7import { setPresence, useOwnPresence } from "../../lib/notify-client";
8import { 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 */
17export 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}