Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
77 changes: 77 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
Loading