fix: fetch emails from "ALL" tab (#71)

Drew Smirnoff created

Change summary

tui/inbox.go      | 77 +++++++++++++++++++++++++++++---------
tui/inbox_test.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 154 insertions(+), 19 deletions(-)

Detailed changes

tui/inbox.go 🔗

@@ -159,6 +159,8 @@ func (m *Inbox) updateList() {
 		showAccountLabel = false
 	}
 
+	m.emailsCount = len(displayEmails)
+
 	items := make([]list.Item, len(displayEmails))
 	for i, email := range displayEmails {
 		accountEmail := ""
@@ -211,21 +213,20 @@ func (m *Inbox) updateList() {
 	}
 
 	m.list = l
-	m.emailsCount = len(displayEmails)
 }
 
 func (m *Inbox) getTitle() string {
 	var title string
 	if m.currentAccountID == "" {
-		title = m.getBaseTitle() + " - All Accounts"
+		title = m.getBaseTitleWithCount() + " - All Accounts"
 	} else {
-		title = m.getBaseTitle()
+		title = m.getBaseTitleWithCount()
 		for _, acc := range m.accounts {
 			if acc.ID == m.currentAccountID {
 				if acc.Name != "" {
-					title = fmt.Sprintf("%s - %s", m.getBaseTitle(), acc.Name)
+					title = fmt.Sprintf("%s - %s", m.getBaseTitleWithCount(), acc.Name)
 				} else {
-					title = fmt.Sprintf("%s - %s", m.getBaseTitle(), acc.Email)
+					title = fmt.Sprintf("%s - %s", m.getBaseTitleWithCount(), acc.Email)
 				}
 				break
 			}
@@ -234,6 +235,9 @@ func (m *Inbox) getTitle() string {
 	if m.isRefreshing {
 		title += " (refreshing...)"
 	}
+	if m.isFetching {
+		title += " (loading more...)"
+	}
 	return title
 }
 
@@ -246,6 +250,10 @@ func (m *Inbox) getBaseTitle() string {
 	}
 }
 
+func (m *Inbox) getBaseTitleWithCount() string {
+	return fmt.Sprintf("%s (%d)", m.getBaseTitle(), m.emailsCount)
+}
+
 func (m *Inbox) Init() tea.Cmd {
 	return nil
 }
@@ -312,7 +320,7 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 	case FetchingMoreEmailsMsg:
 		m.isFetching = true
-		m.list.Title = "Fetching more emails..."
+		m.list.Title = m.getTitle()
 		return m, nil
 
 	case EmailsAppendedMsg:
@@ -376,25 +384,56 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		return m, nil
 	}
 
-	if !m.isFetching && len(m.list.Items()) > 0 && m.list.Index() >= len(m.list.Items())-5 {
-		accountID := m.currentAccountID
-		offset := uint32(m.emailsCount)
-		if accountID == "" && len(m.accounts) > 0 {
-			// For "ALL" view, we'd need to fetch from all accounts
-			// For simplicity, don't auto-fetch more in ALL view
-		} else if accountID != "" {
-			cmds = append(cmds, func() tea.Msg {
-				return FetchMoreEmailsMsg{Offset: offset, AccountID: accountID, Mailbox: m.mailbox}
-			})
-		}
-	}
-
 	var cmd tea.Cmd
 	m.list, cmd = m.list.Update(msg)
 	cmds = append(cmds, cmd)
+
+	if m.shouldFetchMore() {
+		cmds = append(cmds, m.fetchMoreCmds()...)
+	}
 	return m, tea.Batch(cmds...)
 }
 
+func (m *Inbox) shouldFetchMore() bool {
+	if m.isFetching {
+		return false
+	}
+	if len(m.list.Items()) == 0 {
+		return false
+	}
+	if m.list.FilterState() == list.Filtering {
+		return false
+	}
+	return m.list.Index() >= len(m.list.Items())-1
+}
+
+func (m *Inbox) fetchMoreCmds() []tea.Cmd {
+	var cmds []tea.Cmd
+	if m.currentAccountID == "" {
+		if len(m.accounts) == 0 {
+			return nil
+		}
+		for _, acc := range m.accounts {
+			accountID := acc.ID
+			offset := uint32(len(m.emailsByAccount[accountID]))
+			cmds = append(cmds, func(id string, off uint32) tea.Cmd {
+				return func() tea.Msg {
+					return FetchMoreEmailsMsg{Offset: off, AccountID: id, Mailbox: m.mailbox}
+				}
+			}(accountID, offset))
+		}
+		return cmds
+	}
+
+	offset := uint32(len(m.emailsByAccount[m.currentAccountID]))
+	cmds = append(cmds, func(id string, off uint32) tea.Cmd {
+		return func() tea.Msg {
+			return FetchMoreEmailsMsg{Offset: off, AccountID: id, Mailbox: m.mailbox}
+		}
+	}(m.currentAccountID, offset))
+	return cmds
+}
+
 func (m *Inbox) View() string {
 	var b strings.Builder
 

tui/inbox_test.go 🔗

@@ -1,6 +1,7 @@
 package tui
 
 import (
+	"strings"
 	"testing"
 	"time"
 
@@ -9,6 +10,28 @@ import (
 	"github.com/floatpane/matcha/fetcher"
 )
 
+func collectMsgs(cmd tea.Cmd) []tea.Msg {
+	if cmd == nil {
+		return nil
+	}
+	msg := cmd()
+	switch batch := msg.(type) {
+	case tea.BatchMsg:
+		var msgs []tea.Msg
+		for _, m := range batch {
+			if m != nil {
+				msgs = append(msgs, m)
+			}
+		}
+		return msgs
+	default:
+		if msg != nil {
+			return []tea.Msg{msg}
+		}
+	}
+	return nil
+}
+
 // TestInboxUpdate verifies the state transitions in the inbox view.
 func TestInboxUpdate(t *testing.T) {
 	// Create sample accounts
@@ -248,3 +271,76 @@ func TestInboxGetEmailAtIndex(t *testing.T) {
 		t.Error("Expected nil for negative index, got non-nil")
 	}
 }
+
+func TestInboxTitleShowsCount(t *testing.T) {
+	accounts := []config.Account{
+		{ID: "account-1", Email: "test@example.com"},
+	}
+
+	emails := []fetcher.Email{
+		{UID: 1, From: "sender@example.com", Subject: "Email 1", AccountID: "account-1", Date: time.Now()},
+		{UID: 2, From: "sender@example.com", Subject: "Email 2", AccountID: "account-1", Date: time.Now().Add(-time.Minute)},
+		{UID: 3, From: "sender@example.com", Subject: "Email 3", AccountID: "account-1", Date: time.Now().Add(-2 * time.Minute)},
+	}
+
+	inbox := NewInbox(emails, accounts)
+
+	if !strings.Contains(inbox.list.Title, "Inbox (3)") {
+		t.Fatalf("expected inbox title to contain count, got %q", inbox.list.Title)
+	}
+}
+
+func TestSentInboxTitleShowsCount(t *testing.T) {
+	accounts := []config.Account{
+		{ID: "account-1", Email: "test@example.com"},
+	}
+
+	emails := []fetcher.Email{
+		{UID: 1, From: "sender@example.com", Subject: "Email 1", AccountID: "account-1", Date: time.Now()},
+		{UID: 2, From: "sender@example.com", Subject: "Email 2", AccountID: "account-1", Date: time.Now().Add(-time.Minute)},
+	}
+
+	inbox := NewSentInbox(emails, accounts)
+
+	if !strings.Contains(inbox.list.Title, "Sent (2)") {
+		t.Fatalf("expected sent title to contain count, got %q", inbox.list.Title)
+	}
+}
+
+func TestFetchMoreTriggeredAtListEnd(t *testing.T) {
+	accounts := []config.Account{
+		{ID: "account-1", Email: "test@example.com"},
+	}
+
+	emails := []fetcher.Email{
+		{UID: 1, From: "a@example.com", Subject: "Email 1", AccountID: "account-1", Date: time.Now()},
+		{UID: 2, From: "b@example.com", Subject: "Email 2", AccountID: "account-1", Date: time.Now().Add(-time.Minute)},
+	}
+
+	inbox := NewInbox(emails, accounts)
+
+	_, cmd := inbox.Update(tea.KeyMsg{Type: tea.KeyDown})
+	msgs := collectMsgs(cmd)
+
+	var fetchMsg FetchMoreEmailsMsg
+	for _, m := range msgs {
+		if msg, ok := m.(FetchMoreEmailsMsg); ok {
+			fetchMsg = msg
+			break
+		}
+	}
+
+	if fetchMsg.AccountID == "" {
+		t.Fatal("expected a FetchMoreEmailsMsg when reaching end of the list")
+	}
+
+	if fetchMsg.Offset != uint32(len(emails)) {
+		t.Fatalf("expected offset %d, got %d", len(emails), fetchMsg.Offset)
+	}
+	if fetchMsg.AccountID != "account-1" {
+		t.Fatalf("expected account ID 'account-1', got %q", fetchMsg.AccountID)
+	}
+	if fetchMsg.Mailbox != MailboxInbox {
+		t.Fatalf("expected MailboxInbox, got %s", fetchMsg.Mailbox)
+	}
+}