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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user