diff --git a/main.go b/main.go index f371548c..1024f766 100644 --- a/main.go +++ b/main.go @@ -145,6 +145,9 @@ type mainModel struct { sendNotice string pendingAction *pendingEmailAction actionNotice string + // unreadBadge caches the unread count last pushed to the OS badge so the + // value the badge derives from is observable after email operations. + unreadBadge int } type logEntryMsg struct { @@ -296,10 +299,11 @@ func unreadBadgeCount(emailsByAcct, folderEmails map[string][]fetcher.Email) int } func (m *mainModel) syncUnreadBadge() { + count := unreadBadgeCount(m.emailsByAcct, m.folderEmails) + m.unreadBadge = count 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 @@ -1936,6 +1940,8 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo go saveFolderEmailsToCache(folderName, filtered) } + m.syncUnreadBadge() + pa := &pendingEmailAction{ jobID: fmt.Sprintf("action-%d", time.Now().UnixNano()), kind: actionKindDelete, @@ -1986,6 +1992,8 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo go saveFolderEmailsToCache(folderName, filtered) } + m.syncUnreadBadge() + pa := &pendingEmailAction{ jobID: fmt.Sprintf("action-%d", time.Now().UnixNano()), kind: actionKindArchive, @@ -2065,6 +2073,8 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo go saveFolderEmailsToCache(folderName, filtered) } + m.syncUnreadBadge() + pa := &pendingEmailAction{ jobID: fmt.Sprintf("action-%d", time.Now().UnixNano()), kind: actionKindDelete, @@ -2119,6 +2129,8 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo go saveFolderEmailsToCache(folderName, filtered) } + m.syncUnreadBadge() + pa := &pendingEmailAction{ jobID: fmt.Sprintf("action-%d", time.Now().UnixNano()), kind: actionKindArchive, diff --git a/main_test.go b/main_test.go index a7ab1826..984c7801 100644 --- a/main_test.go +++ b/main_test.go @@ -6,7 +6,9 @@ import ( "testing" "unicode/utf8" + "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/fetcher" + "github.com/floatpane/matcha/tui" ) func TestSanitizeFilenameTruncatesCJKOnUTF8Boundary(t *testing.T) { @@ -61,6 +63,81 @@ func TestParseGlobalFlagsDoesNotConsumeSubcommandFlags(t *testing.T) { } } +// newBadgeTestModel builds a minimal mainModel seeded with a single unread +// email for one account, ready to receive delete/archive messages. +func newBadgeTestModel(uid uint32, accountID string) *mainModel { + email := fetcher.Email{UID: uid, AccountID: accountID, IsRead: false} + return &mainModel{ + current: tui.NewChoice(), + config: &config.Config{ + Accounts: []config.Account{{ID: accountID}}, + }, + emails: []fetcher.Email{email}, + emailsByAcct: map[string][]fetcher.Email{accountID: {email}}, + folderEmails: map[string][]fetcher.Email{folderInbox: {email}}, + } +} + +func TestDeleteEmailRefreshesUnreadBadge(t *testing.T) { + const acct = "acct-a" + m := newBadgeTestModel(7, acct) + m.syncUnreadBadge() + if m.unreadBadge != 1 { + t.Fatalf("setup: unreadBadge = %d, want 1", m.unreadBadge) + } + + m.Update(tui.DeleteEmailMsg{UID: 7, AccountID: acct}) + + if m.unreadBadge != 0 { + t.Fatalf("after delete: unreadBadge = %d, want 0 (badge not refreshed)", m.unreadBadge) + } +} + +func TestArchiveEmailRefreshesUnreadBadge(t *testing.T) { + const acct = "acct-a" + m := newBadgeTestModel(7, acct) + m.syncUnreadBadge() + if m.unreadBadge != 1 { + t.Fatalf("setup: unreadBadge = %d, want 1", m.unreadBadge) + } + + m.Update(tui.ArchiveEmailMsg{UID: 7, AccountID: acct}) + + if m.unreadBadge != 0 { + t.Fatalf("after archive: unreadBadge = %d, want 0 (badge not refreshed)", m.unreadBadge) + } +} + +func TestBatchDeleteEmailsRefreshesUnreadBadge(t *testing.T) { + const acct = "acct-a" + m := newBadgeTestModel(7, acct) + m.syncUnreadBadge() + if m.unreadBadge != 1 { + t.Fatalf("setup: unreadBadge = %d, want 1", m.unreadBadge) + } + + m.Update(tui.BatchDeleteEmailsMsg{UIDs: []uint32{7}, AccountID: acct}) + + if m.unreadBadge != 0 { + t.Fatalf("after batch delete: unreadBadge = %d, want 0 (badge not refreshed)", m.unreadBadge) + } +} + +func TestBatchArchiveEmailsRefreshesUnreadBadge(t *testing.T) { + const acct = "acct-a" + m := newBadgeTestModel(7, acct) + m.syncUnreadBadge() + if m.unreadBadge != 1 { + t.Fatalf("setup: unreadBadge = %d, want 1", m.unreadBadge) + } + + m.Update(tui.BatchArchiveEmailsMsg{UIDs: []uint32{7}, AccountID: acct}) + + if m.unreadBadge != 0 { + t.Fatalf("after batch archive: unreadBadge = %d, want 0 (badge not refreshed)", m.unreadBadge) + } +} + func TestUnreadBadgeCountDeduplicatesOverlappingStores(t *testing.T) { email := fetcher.Email{UID: 42, AccountID: "acct-a"} got := unreadBadgeCount(