diff --git a/go.mod b/go.mod index 86bc607ee7208a818308d3c100f746aea650e4a6..1e2400a0eac934e43ec448d2bea435198b025626 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/google/uuid v1.6.0 github.com/yuin/goldmark v1.7.16 github.com/zalando/go-keyring v0.2.6 + go.mozilla.org/pkcs7 v0.9.0 golang.org/x/sys v0.41.0 golang.org/x/text v0.34.0 ) @@ -37,7 +38,6 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.mozilla.org/pkcs7 v0.9.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.19.0 // indirect ) diff --git a/main.go b/main.go index 56a45c77311a629c2179c39d5c1d6e807d28a692..7e49dbe3be28ad54e476252089aff4eb526e5c41 100644 --- a/main.go +++ b/main.go @@ -322,17 +322,51 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tui.EmailsRefreshedMsg: if msg.Mailbox == tui.MailboxSent { - m.sentByAcct = msg.EmailsByAccount - m.sentEmails = flattenAndSort(msg.EmailsByAccount) + for accID, refreshed := range msg.EmailsByAccount { + refreshedUIDs := make(map[uint32]struct{}, len(refreshed)) + for _, e := range refreshed { + refreshedUIDs[e.UID] = struct{}{} + } + if existing, ok := m.sentByAcct[accID]; ok { + for _, e := range existing { + if _, found := refreshedUIDs[e.UID]; !found { + refreshed = append(refreshed, e) + } + } + } + m.sentByAcct[accID] = refreshed + } + m.sentEmails = flattenAndSort(m.sentByAcct) if m.sentInbox != nil { m.sentInbox.SetEmails(m.sentEmails, m.config.Accounts) - m.current, _ = m.current.Update(msg) + // Clear refreshing state on the sent inbox directly so we + // don't accidentally swap m.current away from an email view. + m.sentInbox.Update(msg) } return m, nil } - m.emailsByAcct = msg.EmailsByAccount - m.emails = flattenAndSort(msg.EmailsByAccount) + // Merge refreshed emails with any paginated emails already loaded. + // The refresh only fetches the initial batch; paginated emails beyond + // that must be preserved. + for accID, refreshed := range msg.EmailsByAccount { + refreshedUIDs := make(map[uint32]struct{}, len(refreshed)) + for _, e := range refreshed { + refreshedUIDs[e.UID] = struct{}{} + } + // Keep paginated emails not covered by the refresh + if existing, ok := m.emailsByAcct[accID]; ok { + for _, e := range existing { + if _, found := refreshedUIDs[e.UID]; !found { + refreshed = append(refreshed, e) + } + } + } + m.emailsByAcct[accID] = refreshed + } + // Accounts not in the refresh response (e.g. fetch failed) are + // kept as-is since m.emailsByAcct was not cleared. + m.emails = flattenAndSort(m.emailsByAcct) // Save to cache (inbox only) go saveEmailsToCache(m.emails) @@ -340,8 +374,9 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Update inbox if it exists if m.inbox != nil { m.inbox.SetEmails(m.emails, m.config.Accounts) - // Forward the message to inbox to clear refreshing state - m.current, _ = m.current.Update(msg) + // Clear refreshing state on the inbox directly so we don't + // accidentally swap m.current away from an email view. + m.inbox.Update(msg) } return m, nil @@ -422,12 +457,22 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.emails = flattenAndSort(m.emailsByAcct) if m.inbox == nil { m.inbox = tui.NewInbox(m.emails, m.config.Accounts) - } else { - m.inbox.SetEmails(m.emails, m.config.Accounts) + m.current = m.inbox + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) + return m, m.current.Init() } - m.current = m.inbox - m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) - return m, m.current.Init() + m.inbox.SetEmails(m.emails, m.config.Accounts) + // Only switch to inbox if we're on a loading/status screen, not + // when the user is viewing an email or composing. + if _, onInbox := m.current.(*tui.Inbox); onInbox { + return m, nil + } + if _, onStatus := m.current.(tui.Status); onStatus { + m.current = m.inbox + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) + return m, m.current.Init() + } + return m, nil case tui.FetchMoreEmailsMsg: if msg.AccountID == "" { @@ -604,12 +649,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.current.Init() case tui.ViewEmailMsg: - email := m.getEmailByUIDAndAccount(msg.UID, msg.AccountID, msg.Mailbox) - if email == nil { + if m.getEmailByUIDAndAccount(msg.UID, msg.AccountID, msg.Mailbox) == nil { return m, nil } m.current = tui.NewStatus("Fetching email content...") - return m, tea.Batch(m.current.Init(), fetchEmailBodyCmd(m.config, *email, msg.UID, msg.AccountID, msg.Mailbox)) + return m, tea.Batch(m.current.Init(), fetchEmailBodyCmd(m.config, msg.UID, msg.AccountID, msg.Mailbox)) case tui.EmailBodyFetchedMsg: if msg.Err != nil { @@ -1354,7 +1398,7 @@ func parseEmailAddress(addr string) (name, email string) { return name, email } -func fetchEmailBodyCmd(cfg *config.Config, email fetcher.Email, uid uint32, accountID string, mailbox tui.MailboxKind) tea.Cmd { +func fetchEmailBodyCmd(cfg *config.Config, uid uint32, accountID string, mailbox tui.MailboxKind) tea.Cmd { return func() tea.Msg { account := cfg.GetAccountByID(accountID) if account == nil { diff --git a/tui/inbox.go b/tui/inbox.go index 630848bf72620a97f40de8c0e85f9d095cb92288..afde54e69f5ee63b159ff9d5f905d04617586a6d 100644 --- a/tui/inbox.go +++ b/tui/inbox.go @@ -403,35 +403,11 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Mailbox != m.mailbox { return m, nil } + // Only clear the refreshing indicator. The actual email data is + // merged by the main model (preserving paginated emails) and + // pushed to us via SetEmails, so we must not overwrite it here. m.isRefreshing = false - - // Replace emails with fresh data - m.emailsByAccount = msg.EmailsByAccount - - // Flatten all emails - var allEmails []fetcher.Email - for _, emails := range msg.EmailsByAccount { - allEmails = append(allEmails, emails...) - } - - // Sort by date (newest first) - for i := 0; i < len(allEmails); i++ { - for j := i + 1; j < len(allEmails); j++ { - if allEmails[j].Date.After(allEmails[i].Date) { - allEmails[i], allEmails[j] = allEmails[j], allEmails[i] - } - } - } - - m.allEmails = allEmails - - // Update email counts - m.emailCountByAcct = make(map[string]int) - for accID, accEmails := range m.emailsByAccount { - m.emailCountByAcct[accID] = len(accEmails) - } - - m.updateList() + m.list.Title = m.getTitle() return m, nil }