From cbd11372c1fa1520d46b4592989949f9ce6c53ee Mon Sep 17 00:00:00 2001 From: FromSi Date: Tue, 2 Jun 2026 00:34:51 +0500 Subject: [PATCH] fix(badge): dedupe unread count (#1398) ## What? Deduplicate unread badge counting across `emailsByAcct` and `folderEmails` by tracking seen emails with `AccountID + UID`. Added a regression test for the case where the same unread email exists in both stores. image ## Why? Closes #1107 `syncUnreadBadge` counted unread emails from both stores independently, but the stores can contain the same fetched messages. This could make the macOS unread badge show roughly double the real unread count. image --- main.go | 43 +++++++++++++++++++++++++++++++------------ main_test.go | 18 ++++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 406397a53ac273b95e1e4b09eca16a2507088ef2..07db2972f1f6a1640c0b50a0db01f090e4d08e92 100644 --- a/main.go +++ b/main.go @@ -242,27 +242,46 @@ func waitForLogEntry(ch <-chan logging.Entry) tea.Cmd { } } -func (m *mainModel) syncUnreadBadge() { - if runtime.GOOS != goosDarwin { - return - } +func unreadBadgeCount(emailsByAcct, folderEmails map[string][]fetcher.Email) int { count := 0 + seen := make(map[string]struct{}) + + countUnread := func(e fetcher.Email) { + if e.IsRead { + return + } + key := fmt.Sprintf("%s:%d", e.AccountID, e.UID) + if _, ok := seen[key]; ok { + return + } + seen[key] = struct{}{} + count++ + } + // Count unread across all accounts (cached/loaded emails) - for _, emails := range m.emailsByAcct { + for _, emails := range emailsByAcct { for _, e := range emails { - if !e.IsRead { - count++ - } + countUnread(e) } } // Also check folderEmails for unread status - for _, emails := range m.folderEmails { + for _, emails := range folderEmails { for _, e := range emails { - if !e.IsRead { - count++ - } + countUnread(e) } } + return count +} + +func (m *mainModel) syncUnreadBadge() { + if runtime.GOOS != goosDarwin && loglevel.Get() < loglevel.LevelDebug { + return + } + count := unreadBadgeCount(m.emailsByAcct, m.folderEmails) + loglevel.Debugf("unread badge count: %d", count) + if runtime.GOOS != goosDarwin { + return + } _ = macos.SetBadge(count) } diff --git a/main_test.go b/main_test.go index 6484635ced0bfcc079a463bacc6360c2e81433c6..a7ab18261fbc51a4c19a1bbba5033f517a1c59f3 100644 --- a/main_test.go +++ b/main_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" "unicode/utf8" + + "github.com/floatpane/matcha/fetcher" ) func TestSanitizeFilenameTruncatesCJKOnUTF8Boundary(t *testing.T) { @@ -58,3 +60,19 @@ func TestParseGlobalFlagsDoesNotConsumeSubcommandFlags(t *testing.T) { t.Fatalf("args = %q, want %q", got, "matcha send --logs") } } + +func TestUnreadBadgeCountDeduplicatesOverlappingStores(t *testing.T) { + email := fetcher.Email{UID: 42, AccountID: "acct-a"} + got := unreadBadgeCount( + map[string][]fetcher.Email{ + "acct-a": {email}, + }, + map[string][]fetcher.Email{ + folderInbox: {email}, + }, + ) + + if got != 1 { + t.Fatalf("unreadBadgeCount() = %d, want 1", got) + } +}