#594 meetings alert: replay chime after postpone
The user reported: when a 5-min meeting reminder fires and they hit
"Postpone 5 min", the second reminder appears at the new time but
**without sound** — and the only way to get the sound back is to leave
the app and come back (full tab reload).
Root cause: `UpcomingMeetingAlert` keeps a `playedRef: Set<number>` of
meeting ids that have already chimed, so the 30s polling/refetch
doesn't replay the chime over and over. The alert is mounted globally
in `App.tsx` and never unmounts during in-app navigation, so the Set
lives for the whole session. Postpone changes the meeting's
`startTime` but keeps the same `id`, so the second alert hit the
"already chimed" guard and stayed silent. Only a real tab reload
cleared the Set, which is exactly the workaround the user discovered.
Fix: change the dedupe key from `meeting.id` (number) to
`${meeting.id}:${startTime}` (string). Every postpone moves
startTime, so each post-postpone alert is treated as a fresh
instance and chimes again. The 3s throttle inside
`NotificationPlayer` still protects against burst-replay within the
same alert instance.
Untouched:
- `shownRef` — still keyed by id, because the server-side "shown"
audit row is one-per-meeting-per-user by design.
- `NotificationPlayer`, iOS unlock path, vibration, throttle.
- Postpone API surface and `PostponeDialog`.
Verified: `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
This commit is contained in:
@@ -455,21 +455,32 @@ export function UpcomingMeetingAlert() {
|
||||
}, [eligibleId]);
|
||||
|
||||
// Play the meeting reminder sound exactly once per *new* eligible
|
||||
// meeting (deduped by id) so polling refetches don't replay the chime
|
||||
// every 30s. Plays whether the tab is foreground or background — the
|
||||
// user wants to hear the reminder either way. Honors mute and the
|
||||
// alert-instance so polling refetches don't replay the chime every
|
||||
// 30s. Plays whether the tab is foreground or background — the user
|
||||
// wants to hear the reminder either way. Honors mute and the
|
||||
// per-channel meeting toggle.
|
||||
const playedRef = useRef<Set<number>>(new Set());
|
||||
//
|
||||
// #594: the dedupe key intentionally includes `startTime`, not just
|
||||
// `meeting.id`. When the user postpones a meeting by N minutes, the
|
||||
// server keeps the same `id` but moves `startTime` forward. Without
|
||||
// the start-time in the key, the second alert (at the new time) was
|
||||
// treated as "already chimed" and stayed silent — only a full tab
|
||||
// reload would clear `playedRef` and restore the chime. Keying on
|
||||
// `id:startTime` makes every postpone (and any future reschedule)
|
||||
// count as a fresh alert instance and chime again.
|
||||
const playedRef = useRef<Set<string>>(new Set());
|
||||
const eligibleStartTime = eligible?.meeting.startTime ?? null;
|
||||
useEffect(() => {
|
||||
if (!eligibleId || !user) return;
|
||||
if (playedRef.current.has(eligibleId)) return;
|
||||
playedRef.current.add(eligibleId);
|
||||
if (!eligibleId || !eligibleStartTime || !user) return;
|
||||
const key = `${eligibleId}:${eligibleStartTime}`;
|
||||
if (playedRef.current.has(key)) return;
|
||||
playedRef.current.add(key);
|
||||
if (user.notificationsMuted) return;
|
||||
if (!user.notifyMeetingsEnabled) return;
|
||||
notificationPlayer.play(user.notificationSoundMeeting, {
|
||||
vibrate: user.vibrationEnabledMeeting,
|
||||
});
|
||||
}, [eligibleId, user]);
|
||||
}, [eligibleId, eligibleStartTime, user]);
|
||||
|
||||
if (!enabled || !eligible) return null;
|
||||
// User toggled the popup off in settings — render nothing. Detection
|
||||
|
||||
Reference in New Issue
Block a user