From 3f324a14d26ae9c4f78b6d78002e32c2f75acb27 Mon Sep 17 00:00:00 2001 From: Gianluca Iavicoli Date: Mon, 30 Mar 2026 00:11:53 +0200 Subject: [PATCH] fix: bridge mark-as-unread/read state between Matrix and WhatsApp --- pkg/connector/client.go | 7 +++++++ pkg/connector/handlematrix.go | 20 ++++++++++++++++++-- pkg/connector/handlewhatsapp.go | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 367ad147..00440a03 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -55,6 +55,7 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. pushNamesSynced: exsync.NewEvent(), createDedup: exsync.NewSet[types.MessageID](), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), + recentlyMarkedUnread: make(map[types.JID]time.Time), } login.Client = w @@ -121,6 +122,12 @@ type WhatsAppClient struct { appStateRecoveryLock sync.Mutex appStateFullSyncAttempted map[appstate.WAPatchName]time.Time + + // recentlyMarkedUnread tracks rooms recently marked as unread via WhatsApp + // AppState, so that the spurious ReadReceipt that WhatsApp sends immediately + // after a mark-as-unread can be suppressed. + recentlyMarkedUnread map[types.JID]time.Time + recentlyMarkedUnreadLock sync.Mutex } var ( diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index a330a98e..1cfcf647 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -635,11 +635,27 @@ func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2. if err != nil { return err } - lastTS, lastKey, err := wa.getLastMessageInfo(ctx, chatJID, msg.Portal.PortalKey) + // Use LID as AppState target when available, as WhatsApp may use LID-indexed + // AppState entries for contacts that have been migrated. + targetJID := chatJID + if chatJID.Server == types.DefaultUserServer { + if lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, chatJID); err == nil && !lid.IsEmpty() { + targetJID = lid.ToNonAD() + } + } + lastTS, lastKey, err := wa.getLastMessageInfo(ctx, targetJID, msg.Portal.PortalKey) if err != nil { return err } - return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(chatJID, msg.Content.Unread, lastTS, lastKey)) + // Track mark-as-unread BEFORE sending to WhatsApp so the spurious ReadReceipt + // that WhatsApp sends in response is suppressed. Use the phone JID as key + // since handleWAMarkChatAsRead converts LIDs to phone JIDs. + if msg.Content.Unread { + wa.recentlyMarkedUnreadLock.Lock() + wa.recentlyMarkedUnread[chatJID] = time.Now() + wa.recentlyMarkedUnreadLock.Unlock() + } + return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(targetJID, !msg.Content.Unread, lastTS, lastKey)) } func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error { diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index eb42f439..5e4cb537 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -636,14 +636,41 @@ func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.D func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *events.MarkChatAsRead) bool { chatJID := wa.maybeConvertJIDToLID(ctx, evt.JID) - return wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ + if evt.Action.GetRead() { + // Suppress ReadReceipts that arrive right after a MarkUnread for the same room. + // WhatsApp sends both a mark-as-unread and a read-receipt AppState patch when + // processing a mark-as-unread request. The read-receipt is spurious and would + // undo the mark-as-unread on the Matrix side. + wa.recentlyMarkedUnreadLock.Lock() + markedAt, wasRecent := wa.recentlyMarkedUnread[chatJID] + wa.recentlyMarkedUnreadLock.Unlock() + if wasRecent && time.Since(markedAt) < 5*time.Second { + zerolog.Ctx(ctx).Debug(). + Stringer("chat_jid", chatJID). + Msg("Suppressing spurious ReadReceipt after MarkUnread") + return true + } + return wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ + EventMeta: simplevent.EventMeta{ + Type: bridgev2.RemoteEventReadReceipt, + PortalKey: wa.makeWAPortalKey(chatJID), + Sender: wa.makeEventSender(ctx, wa.JID), + Timestamp: evt.Timestamp, + }, + ReadUpTo: evt.Timestamp, + }).Success + } + wa.recentlyMarkedUnreadLock.Lock() + wa.recentlyMarkedUnread[chatJID] = time.Now() + wa.recentlyMarkedUnreadLock.Unlock() + return wa.UserLogin.QueueRemoteEvent(&simplevent.MarkUnread{ EventMeta: simplevent.EventMeta{ - Type: bridgev2.RemoteEventReadReceipt, + Type: bridgev2.RemoteEventMarkUnread, PortalKey: wa.makeWAPortalKey(chatJID), Sender: wa.makeEventSender(ctx, wa.JID), Timestamp: evt.Timestamp, }, - ReadUpTo: evt.Timestamp, + Unread: true, }).Success }