Skip to content
60 linesCodeBlameRaw
1/**
2 * Sounds and desktop notifications: what a person hears when chat moves,
3 * kept with their account by identity (`sound_settings` /
4 * `set_sound_settings`) so a phone, a laptop and the desktop app all sound
5 * the same. Which sound plays when is the web app's rule
6 * (apps/web/app/lib/chat-sounds.ts); identity keeps the choices. Do not
7 * disturb is not here: it is the person's presence (`dnd_until`,
8 * services/notify), which also silences pop-ups and pushes.
9 *
10 * On its own, with no imports, so node tests can load it.
11 */
12
13/**
14 * The sounds, by what they are for. `message`: a new message in a
15 * conversation you have open but are not looking at. `direct`: a direct
16 * message. `mention`: you were @mentioned. `agent_done`: an agent finished
17 * something for you. `sent`: a soft tick as you send (off unless turned
18 * on). `call` is reserved for calls and has no setting yet.
19 */
20export type SoundCue = "message" | "direct" | "mention" | "agent_done" | "sent" | "call";
21
22/** The cues a person can turn on and off, in the order the settings list them. */
23export const SOUND_CUES: readonly Exclude<SoundCue, "call">[] = ["message", "direct", "mention", "agent_done", "sent"];
24
25/** A set of sounds: the same roles, a different character. */
26export type SoundSet = "soft" | "bright";
27
28export const SOUND_SETS: readonly SoundSet[] = ["soft", "bright"];
29
30export type SoundSettings = {
31 /** Whether anything plays at all. */
32 sounds_enabled: boolean;
33 sound_set: SoundSet;
34 /** 0 to 100. */
35 sound_volume: number;
36 /** Each cue on or off; a cue left out is on, except `sent`. */
37 sound_cues: Partial<Record<SoundCue, boolean>>;
38 /**
39 * Whether an open tab shows a system notification (the browser's
40 * Notifications API) for a message to you while the window is not in
41 * front. Off unless turned on; the browser's permission is asked only
42 * from the settings page.
43 */
44 desktop_toasts: boolean;
45};
46
47/** A change to them: only what is sent changes; `sound_cues` merges by cue. */
48export type SoundSettingsChange = Partial<Omit<SoundSettings, "sound_cues">> & { sound_cues?: Partial<Record<SoundCue, boolean>> };
49
50/** Which cues play before anyone changed them: all but the tick as you send. */
51export const DEFAULT_SOUND_CUES: Record<Exclude<SoundCue, "call">, boolean> = { message: true, direct: true, mention: true, agent_done: true, sent: false };
52
53export const DEFAULT_SOUND_SETTINGS: SoundSettings = { sounds_enabled: true, sound_set: "soft", sound_volume: 60, sound_cues: { ...DEFAULT_SOUND_CUES }, desktop_toasts: false };
54
55/** Whether a cue plays under these settings: its own switch, else its default. */
56export function cueOn(settings: Pick<SoundSettings, "sound_cues">, cue: SoundCue): boolean {
57 const chosen = settings.sound_cues[cue];
58 if (typeof chosen === "boolean") return chosen;
59 return cue === "call" ? true : DEFAULT_SOUND_CUES[cue];
60}