fix(badge): dedupe unread count (#1398)
FromSi
created 1 month ago
## 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"
/>
Change summary
main.go | 43 +++++++++++++++++++++++++++++++------------
main_test.go | 18 ++++++++++++++++++
2 files changed, 49 insertions(+), 12 deletions(-)
Detailed changes
@@ -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)
}
@@ -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)
+ }
+}