From f98969c1ca2d0d681adcdfb8d8de99d1b84db4af Mon Sep 17 00:00:00 2001 From: leocavalcante Date: Thu, 2 Apr 2026 12:01:12 -0300 Subject: [PATCH] feat: archive message --- api/client.go | 12 ++++++++++++ ui/commands.go | 10 ++++++++++ ui/keys.go | 4 +++- ui/update.go | 29 +++++++++++++++++++++++------ ui/view.go | 4 ++-- 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/api/client.go b/api/client.go index 2ec70f0..3ef87a2 100644 --- a/api/client.go +++ b/api/client.go @@ -102,6 +102,18 @@ func (c *Client) DeleteEmail(id string) error { return err } +// ArchiveEmail removes the INBOX label, effectively archiving the email. +func (c *Client) ArchiveEmail(id string) error { + mod := &gmail.ModifyMessageRequest{ + RemoveLabelIds: []string{"INBOX"}, + } + _, err := c.srv.Users.Messages.Modify("me", id, mod).Do() + if err == nil { + c.cache.Delete(id) + } + return err +} + // ToggleRead flips the UNREAD label. Returns the new isUnread state. func (c *Client) ToggleRead(id string, currentlyUnread bool) (newUnread bool, err error) { mod := &gmail.ModifyMessageRequest{} diff --git a/ui/commands.go b/ui/commands.go index a604b0e..9cae3c0 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -21,6 +21,7 @@ type inboxLoadedMsg struct{ emails []api.Email } type emailOpenedMsg struct{ email *api.Email } type emailSentMsg struct{} type emailDeletedMsg struct{ id string } +type emailArchivedMsg struct{ id string } type readToggledMsg struct { id string isUnread bool @@ -72,6 +73,15 @@ func deleteEmailCmd(client *api.Client, id string) tea.Cmd { } } +func archiveEmailCmd(client *api.Client, id string) tea.Cmd { + return func() tea.Msg { + if err := client.ArchiveEmail(id); err != nil { + return errMsg{err: err} + } + return emailArchivedMsg{id: id} + } +} + func toggleReadCmd(client *api.Client, id string, currentlyUnread bool) tea.Cmd { return func() tea.Msg { newUnread, err := client.ToggleRead(id, currentlyUnread) diff --git a/ui/keys.go b/ui/keys.go index 252ca5b..abc52eb 100644 --- a/ui/keys.go +++ b/ui/keys.go @@ -9,6 +9,7 @@ type keyMap struct { Back key.Binding Reply key.Binding Compose key.Binding + Archive key.Binding Delete key.Binding Search key.Binding Labels key.Binding @@ -32,7 +33,7 @@ func (k keyMap) ShortHelp() []key.Binding { func (k keyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ {k.Compose, k.Reply, k.Search, k.Labels}, - {k.Delete, k.ToggleRead, k.Back, k.Quit}, + {k.Archive, k.Delete, k.ToggleRead, k.Back, k.Quit}, {k.Send, k.NextInput, k.PrevInput}, {k.AddAttachment, k.RemoveAttachment, k.DownloadAttachment}, } @@ -44,6 +45,7 @@ var keys = keyMap{ Back: key.NewBinding(key.WithKeys("b", "esc"), key.WithHelp("b/esc", "back")), Reply: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "reply")), Compose: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "compose")), + Archive: key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "archive")), Delete: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "delete")), Search: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "search")), Labels: key.NewBinding(key.WithKeys("l"), key.WithHelp("l", "labels")), diff --git a/ui/update.go b/ui/update.go index 3000e10..438d8fb 100644 --- a/ui/update.go +++ b/ui/update.go @@ -45,6 +45,20 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.statusMessage = "Email sent!" return m, tea.Batch(m.spinner.Tick, fetchInbox(m.client), clearStatusAfter(3*time.Second)) + case emailArchivedMsg: + items := m.emailList.Items() + for i, item := range items { + if ei, ok := item.(emailItem); ok && ei.email.ID == msg.id { + m.emailList.RemoveItem(i) + break + } + } + if m.state == stateViewing { + m.state = stateInbox + } + m.statusMessage = "Email archived" + return m, clearStatusAfter(3 * time.Second) + case emailDeletedMsg: items := m.emailList.Items() for i, item := range items { @@ -190,15 +204,13 @@ func (m Model) updateInbox(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, tea.Batch(m.spinner.Tick, openEmail(m.client, ei.email.ID)) } - case key.Matches(msg, keys.Delete): + case key.Matches(msg, keys.Archive): if ei, ok := m.emailList.SelectedItem().(emailItem); ok { - m.confirmDelete = true - m.deleteTargetID = ei.email.ID - m.deleteSubject = ei.email.Subject - m.statusMessage = fmt.Sprintf("Delete \"%s\"? [y/n]", truncate(ei.email.Subject, 40)) - return m, nil + return m, archiveEmailCmd(m.client, ei.email.ID) } + case key.Matches(msg, keys.Delete): + case key.Matches(msg, keys.ToggleRead): if ei, ok := m.emailList.SelectedItem().(emailItem); ok { return m, toggleReadCmd(m.client, ei.email.ID, ei.email.IsUnread) @@ -226,6 +238,11 @@ func (m Model) updateViewing(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.replyBody.Focus() return m, nil + case key.Matches(msg, keys.Archive): + if m.currentEmail != nil { + return m, archiveEmailCmd(m.client, m.currentEmail.ID) + } + case key.Matches(msg, keys.Delete): m.confirmDelete = true m.deleteTargetID = m.currentEmail.ID diff --git a/ui/view.go b/ui/view.go index 11ca956..aa6e4d5 100644 --- a/ui/view.go +++ b/ui/view.go @@ -64,7 +64,7 @@ func (m Model) loadingView() string { } func (m Model) inboxView() string { - help := "\n[c] compose • [d] delete • [m] mark read/unread • [l] labels • [/] search • [?] help • [q] quit\n" + help := "\n[c] compose • [a] archive • [d] delete • [m] mark read/unread • [l] labels • [/] search • [?] help • [q] quit\n" return m.emailList.View() + m.statusBar() + help } @@ -99,7 +99,7 @@ func (m Model) emailView() string { } b.WriteString(m.statusBar()) - b.WriteString("\n[esc] back • [r] reply • [d] delete • [m] mark read/unread • [ctrl+d] download attachment • [q] quit\n") + b.WriteString("\n[esc] back • [r] reply • [a] archive • [d] delete • [m] mark read/unread • [ctrl+d] download attachment • [q] quit\n") return b.String() }