From 07b0c410abce3400ec95d2af634662d5beacae12 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 7 Jul 2026 12:10:33 -0400 Subject: [PATCH] fix(notifications): don't badge/notify stale device-verification requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In-room verification requests arrive as m.room.message with msgtype 'm.key.verification.request', so isNotificationEvent() counted them (type is m.room.message). A stale/old request at the tail of a DM therefore re-lit the room's unread dot — and could fire a toast/OS notification — on every fresh sync (app update / CTRL+F5 cache clear); opening the room only cleared it via the local read-receipt echo, so it returned on the next reload. Exclude that msgtype from isNotificationEvent so verification control messages never drive unread/notifications (mirrors the existing member/redaction/edit exclusions). The rest of the verification flow already uses distinct event types that aren't notification events. Co-Authored-By: Claude Opus 4.8 --- src/app/utils/room.test.ts | 22 ++++++++++++++++++++++ src/app/utils/room.ts | 9 +++++++++ 2 files changed, 31 insertions(+) diff --git a/src/app/utils/room.test.ts b/src/app/utils/room.test.ts index 85d73d914..2e2cbcceb 100644 --- a/src/app/utils/room.test.ts +++ b/src/app/utils/room.test.ts @@ -357,6 +357,28 @@ test('isNotificationEvent accepts message/sticker but rejects member, redacted, ), false, ); + + // Device-verification requests (m.room.message + this msgtype) are crypto + // control messages — must NOT count as unread/notify (stale-request nag fix). + assert.equal( + isNotificationEvent( + mockEvent({ + getType: () => 'm.room.message', + getContent: () => ({ msgtype: 'm.key.verification.request' }), + }), + ), + false, + ); + // Regression guard: a normal text message still notifies. + assert.equal( + isNotificationEvent( + mockEvent({ + getType: () => 'm.room.message', + getContent: () => ({ msgtype: 'm.text', body: 'hi' }), + }), + ), + true, + ); }); // --- roomHaveNotification / getUnreadInfo -------------------------------- diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index a957328da..fc03b78b5 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -195,12 +195,21 @@ const NOTIFICATION_EVENT_TYPES = [ 'm.room.member', 'm.sticker', ]; +// In-room device-verification requests are sent as m.room.message with this +// msgtype (the rest of the flow — start/accept/key/mac/done/cancel — uses its own +// event types, already excluded above). They're crypto control messages, not chat. +const VERIFICATION_REQUEST_MSGTYPE = 'm.key.verification.request'; export const isNotificationEvent = (mEvent: MatrixEvent) => { const eType = mEvent.getType(); if (!NOTIFICATION_EVENT_TYPES.includes(eType)) { return false; } if (eType === 'm.room.member') return false; + // Don't badge/notify a verification request — otherwise a stale one at the tail + // of a DM re-lights the room's unread dot on every fresh sync (cache clear). + if (eType === 'm.room.message' && mEvent.getContent().msgtype === VERIFICATION_REQUEST_MSGTYPE) { + return false; + } if (mEvent.isRedacted()) return false; if (mEvent.getRelation()?.rel_type === 'm.replace') return false;