Skip to content

Commit cbd1137

Browse files
authored
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. <img width="595" height="652" alt="image" src="https://github.com/user-attachments/assets/8c837fb8-017c-4c7c-aa2c-052f244288b2" /> ## 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. <img width="598" height="647" alt="image" src="https://github.com/user-attachments/assets/f2b1c267-29bc-4d4c-a116-4c91af789722" />
1 parent e4987f3 commit cbd1137

2 files changed

Lines changed: 49 additions & 12 deletions

File tree

main.go

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -242,27 +242,46 @@ func waitForLogEntry(ch <-chan logging.Entry) tea.Cmd {
242242
}
243243
}
244244

245-
func (m *mainModel) syncUnreadBadge() {
246-
if runtime.GOOS != goosDarwin {
247-
return
248-
}
245+
func unreadBadgeCount(emailsByAcct, folderEmails map[string][]fetcher.Email) int {
249246
count := 0
247+
seen := make(map[string]struct{})
248+
249+
countUnread := func(e fetcher.Email) {
250+
if e.IsRead {
251+
return
252+
}
253+
key := fmt.Sprintf("%s:%d", e.AccountID, e.UID)
254+
if _, ok := seen[key]; ok {
255+
return
256+
}
257+
seen[key] = struct{}{}
258+
count++
259+
}
260+
250261
// Count unread across all accounts (cached/loaded emails)
251-
for _, emails := range m.emailsByAcct {
262+
for _, emails := range emailsByAcct {
252263
for _, e := range emails {
253-
if !e.IsRead {
254-
count++
255-
}
264+
countUnread(e)
256265
}
257266
}
258267
// Also check folderEmails for unread status
259-
for _, emails := range m.folderEmails {
268+
for _, emails := range folderEmails {
260269
for _, e := range emails {
261-
if !e.IsRead {
262-
count++
263-
}
270+
countUnread(e)
264271
}
265272
}
273+
return count
274+
}
275+
276+
func (m *mainModel) syncUnreadBadge() {
277+
if runtime.GOOS != goosDarwin && loglevel.Get() < loglevel.LevelDebug {
278+
return
279+
}
280+
count := unreadBadgeCount(m.emailsByAcct, m.folderEmails)
281+
loglevel.Debugf("unread badge count: %d", count)
282+
if runtime.GOOS != goosDarwin {
283+
return
284+
}
266285
_ = macos.SetBadge(count)
267286
}
268287

main_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strings"
66
"testing"
77
"unicode/utf8"
8+
9+
"github.com/floatpane/matcha/fetcher"
810
)
911

1012
func TestSanitizeFilenameTruncatesCJKOnUTF8Boundary(t *testing.T) {
@@ -58,3 +60,19 @@ func TestParseGlobalFlagsDoesNotConsumeSubcommandFlags(t *testing.T) {
5860
t.Fatalf("args = %q, want %q", got, "matcha send --logs")
5961
}
6062
}
63+
64+
func TestUnreadBadgeCountDeduplicatesOverlappingStores(t *testing.T) {
65+
email := fetcher.Email{UID: 42, AccountID: "acct-a"}
66+
got := unreadBadgeCount(
67+
map[string][]fetcher.Email{
68+
"acct-a": {email},
69+
},
70+
map[string][]fetcher.Email{
71+
folderInbox: {email},
72+
},
73+
)
74+
75+
if got != 1 {
76+
t.Fatalf("unreadBadgeCount() = %d, want 1", got)
77+
}
78+
}

0 commit comments

Comments
 (0)