fix(correctness): call-join reset on embed swap + per-path notify dedupe
COR-2 (useCallEmbed): useCallJoined only reset `joined` when the embed became
undefined. Answering a 2nd call swaps the embed A->B directly (embed stays
truthy), so `joined` stayed true and call B rendered as already-joined,
skipping the loading/watchdog UI. Re-seed from `embed?.joined ?? false` on
every embed identity change.
COR-4 (ClientNonUIFeatures): the notify-dedupe used one Map<roomId,eventId>
slot shared by the main-timeline and per-thread paths, so a thread reply
overwrote the room's slot and a re-fired main message (decrypt/edit re-emit,
common in E2EE) then mismatched and double-notified. Key the slot by
`${roomId}|${threadId ?? 'main'}` so each path dedupes independently.
Both verified correct by two review passes (no missed-notification or
missed-join regressions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -149,9 +149,11 @@ export const useCallJoined = (embed?: CallEmbed): boolean => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!embed) {
|
||||
setJoined(false);
|
||||
}
|
||||
// Re-seed from the (new) embed whenever it changes identity, not only when
|
||||
// it becomes undefined. Answering a 2nd call swaps embed A→B directly (embed
|
||||
// stays truthy), so a plain `if (!embed)` reset left `joined` stuck true and
|
||||
// B rendered as already-joined, skipping the loading/watchdog UI.
|
||||
setJoined(embed?.joined ?? false);
|
||||
}, [embed]);
|
||||
|
||||
return joined;
|
||||
|
||||
@@ -373,8 +373,10 @@ function StatusExpiryMonitor() {
|
||||
|
||||
function MessageNotifications() {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
// Notify dedupe, keyed `${roomId}|${threadId ?? 'main'}` -> last notified
|
||||
// eventId, so the main timeline and each thread dedupe independently.
|
||||
const lastNotifiedEventRef = useRef<Map<string, string>>(new Map());
|
||||
// Per-thread dedupe: threadId -> last notified eventId.
|
||||
// Per-thread dedupe (thread-detection gate): threadId -> last notified eventId.
|
||||
const lastNotifiedThreadRef = useRef<Map<string, string>>(new Map());
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
@@ -513,12 +515,16 @@ function MessageNotifications() {
|
||||
const eventId = mEvent.getId();
|
||||
if (!sender || !eventId) return;
|
||||
|
||||
// Dedupe on the event id (per room): the same event can re-fire (decryption,
|
||||
// edit, thread repopulation). This replaces the old unread-COUNT dedupe,
|
||||
// which suppressed a genuinely-new message whenever its post-read count
|
||||
// matched the previously-notified count — i.e. "read a DM, next message
|
||||
// never notifies/sounds" (the common one-at-a-time cadence).
|
||||
if (lastNotifiedEventRef.current.get(room.roomId) === eventId) return;
|
||||
// Dedupe on the event id (per room AND per path): the same event can
|
||||
// re-fire (decryption, edit, thread repopulation). The main timeline and
|
||||
// each thread get their own slot — a shared per-room slot let a thread
|
||||
// reply overwrite the main slot, so a re-fired main message then mismatched
|
||||
// and double-notified. This replaces the old unread-COUNT dedupe, which
|
||||
// suppressed a genuinely-new message whenever its post-read count matched
|
||||
// the previously-notified count (the "read a DM, next message never
|
||||
// notifies/sounds" one-at-a-time cadence).
|
||||
const dedupeKey = `${room.roomId}|${threadId ?? 'main'}`;
|
||||
if (lastNotifiedEventRef.current.get(dedupeKey) === eventId) return;
|
||||
|
||||
// Main-timeline path respects push rules: don't notify when the room has no
|
||||
// notification count (e.g. a non-mention in a Mentions-only room). The
|
||||
@@ -527,7 +533,7 @@ function MessageNotifications() {
|
||||
// override in a Mentions-only room is silently dropped.
|
||||
if (!threadId && getUnreadInfo(room).total === 0) return;
|
||||
|
||||
lastNotifiedEventRef.current.set(room.roomId, eventId);
|
||||
lastNotifiedEventRef.current.set(dedupeKey, eventId);
|
||||
|
||||
const quietActive =
|
||||
focusAssistActive ||
|
||||
|
||||
Reference in New Issue
Block a user