Task #393: Coalesce notification sound bursts on app open

User reported a flood of chimes when opening Tx OS while several
notifications were pending. Two complementary fixes:

1. Player throttle bumped from 600 ms → 3000 ms in
   artifacts/tx-os/src/lib/notification-sounds.ts. Bursts of real-time
   events now produce a single chime instead of several overlapping ones.

2. Socket warmup gate added in
   artifacts/tx-os/src/hooks/use-notifications-socket.ts:
   - Track connectedAtRef (useRef<number>), set on effect mount and on
     each socket "connect" event using performance.now().
   - In the notification_created handler, after mute / per-channel
     gates but before notificationPlayer.play(...), suppress
     audio + vibration when within the 3 s warmup window.
   - UI/badge invalidation still runs — only sound is silenced during
     the warmup, so the user sees notifications appear without an
     alarm-like welcome.

All existing gates preserved (global mute, per-channel sound/vibration,
autoplay-blocked toast hint, per-meeting playedRef dedupe).

No deviations. tx-os typecheck passes.
This commit is contained in:
Riyadh
2026-05-05 12:30:00 +00:00
parent f73db63964
commit 83a70de0d9
2 changed files with 23 additions and 4 deletions
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { io } from "socket.io-client";
import { useQueryClient } from "@tanstack/react-query";
import {
@@ -19,17 +19,31 @@ import { notificationPlayer } from "@/lib/notification-sounds";
const BASE = import.meta.env.BASE_URL.replace(/\/$/, "");
const SOCKET_WARMUP_MS = 3000;
const nowMs = () =>
typeof performance !== "undefined" ? performance.now() : Date.now();
export function useNotificationsSocket() {
const { user } = useAuth();
const queryClient = useQueryClient();
const connectedAtRef = useRef<number>(0);
useEffect(() => {
if (!user) return;
// Suppress chimes for the first few seconds after the socket
// connects so any pending notifications that flush in on page
// load don't all play sounds back-to-back.
connectedAtRef.current = nowMs();
const socket = io(window.location.origin, {
path: `${BASE}/api/socket.io`,
});
socket.on("connect", () => {
connectedAtRef.current = nowMs();
});
socket.on("notification_created", (payload?: { type?: string }) => {
queryClient.invalidateQueries({ queryKey: getListNotificationsQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetHomeStatsQueryKey() });
@@ -52,6 +66,10 @@ export function useNotificationsSocket() {
if (isOrder && !user.notifyOrdersEnabled) return;
if (isMeeting && !user.notifyMeetingsEnabled) return;
if (!isOrder && !isMeeting) return;
// Skip audio/vibration during the warmup window — UI/badge
// counts (above) still update, but we stay silent so a
// page-load flush doesn't sound like an alarm.
if (nowMs() - connectedAtRef.current < SOCKET_WARMUP_MS) return;
const soundId = isOrder
? user.notificationSoundOrder
: user.notificationSoundMeeting;