email_view.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/charmbracelet/bubbles/viewport"
  8	tea "github.com/charmbracelet/bubbletea"
  9	"github.com/charmbracelet/lipgloss"
 10	"github.com/floatpane/matcha/fetcher"
 11	"github.com/floatpane/matcha/view"
 12)
 13
 14var (
 15	emailHeaderStyle   = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).Padding(0, 1)
 16	attachmentBoxStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, false, true).PaddingLeft(2).MarginTop(1)
 17)
 18
 19type EmailView struct {
 20	viewport           viewport.Model
 21	email              fetcher.Email
 22	emailIndex         int
 23	attachmentCursor   int
 24	focusOnAttachments bool
 25	accountID          string
 26	mailbox            MailboxKind
 27}
 28
 29func NewEmailView(email fetcher.Email, emailIndex, width, height int, mailbox MailboxKind) *EmailView {
 30	// Pass the styles from the tui package to the view package
 31	body, err := view.ProcessBody(email.Body, H1Style, H2Style, BodyStyle)
 32	if err != nil {
 33		body = fmt.Sprintf("Error rendering body: %v", err)
 34	}
 35
 36	// Create header and compute heights that reduce viewport space.
 37	header := fmt.Sprintf("From: %s\nSubject: %s", email.From, email.Subject)
 38	headerHeight := lipgloss.Height(header) + 2
 39
 40	attachmentHeight := 0
 41	if len(email.Attachments) > 0 {
 42		attachmentHeight = len(email.Attachments) + 2
 43	}
 44
 45	// Build viewport with initial size and set wrapped content to fit the width.
 46	vp := viewport.New(width, height-headerHeight-attachmentHeight)
 47
 48	// Wrap the body to the viewport width so lines don't run off-screen.
 49	wrapped := wrapBodyToWidth(body, vp.Width)
 50	vp.SetContent(wrapped)
 51
 52	return &EmailView{
 53		viewport:   vp,
 54		email:      email,
 55		emailIndex: emailIndex,
 56		accountID:  email.AccountID,
 57		mailbox:    mailbox,
 58	}
 59}
 60
 61func (m *EmailView) Init() tea.Cmd {
 62	return nil
 63}
 64
 65func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 66	var cmd tea.Cmd
 67	var cmds []tea.Cmd
 68
 69	switch msg := msg.(type) {
 70	case tea.KeyMsg:
 71		// Handle 'esc' key locally
 72		if msg.Type == tea.KeyEsc {
 73			if m.focusOnAttachments {
 74				m.focusOnAttachments = false
 75				return m, nil
 76			}
 77			return m, func() tea.Msg { return BackToMailboxMsg{Mailbox: m.mailbox} }
 78		}
 79
 80		if m.focusOnAttachments {
 81			switch msg.String() {
 82			case "up", "k":
 83				if m.attachmentCursor > 0 {
 84					m.attachmentCursor--
 85				}
 86			case "down", "j":
 87				if m.attachmentCursor < len(m.email.Attachments)-1 {
 88					m.attachmentCursor++
 89				}
 90			case "enter":
 91				if len(m.email.Attachments) > 0 {
 92					selected := m.email.Attachments[m.attachmentCursor]
 93					idx := m.emailIndex
 94					accountID := m.accountID
 95					return m, func() tea.Msg {
 96						return DownloadAttachmentMsg{
 97							Index:     idx,
 98							Filename:  selected.Filename,
 99							PartID:    selected.PartID,
100							Data:      selected.Data,
101							AccountID: accountID,
102							Mailbox:   m.mailbox,
103						}
104					}
105				}
106			case "tab":
107				m.focusOnAttachments = false
108			}
109		} else {
110			switch msg.String() {
111			case "r":
112				return m, func() tea.Msg { return ReplyToEmailMsg{Email: m.email} }
113			case "d":
114				accountID := m.accountID
115				uid := m.email.UID
116				return m, func() tea.Msg {
117					return DeleteEmailMsg{UID: uid, AccountID: accountID, Mailbox: m.mailbox}
118				}
119			case "a":
120				accountID := m.accountID
121				uid := m.email.UID
122				return m, func() tea.Msg {
123					return ArchiveEmailMsg{UID: uid, AccountID: accountID, Mailbox: m.mailbox}
124				}
125			case "tab":
126				if len(m.email.Attachments) > 0 {
127					m.focusOnAttachments = true
128				}
129			}
130		}
131	case tea.WindowSizeMsg:
132		header := fmt.Sprintf("From: %s\nSubject: %s", m.email.From, m.email.Subject)
133		headerHeight := lipgloss.Height(header) + 2
134		attachmentHeight := 0
135		if len(m.email.Attachments) > 0 {
136			attachmentHeight = len(m.email.Attachments) + 2
137		}
138		// Update viewport dimensions
139		m.viewport.Width = msg.Width
140		m.viewport.Height = msg.Height - headerHeight - attachmentHeight
141
142		// When the window size changes, rewrap the body to the new width
143		body, err := view.ProcessBody(m.email.Body, H1Style, H2Style, BodyStyle)
144		if err != nil {
145			body = fmt.Sprintf("Error rendering body: %v", err)
146		}
147		wrapped := wrapBodyToWidth(body, m.viewport.Width)
148		m.viewport.SetContent(wrapped)
149	}
150
151	m.viewport, cmd = m.viewport.Update(msg)
152	cmds = append(cmds, cmd)
153
154	return m, tea.Batch(cmds...)
155}
156
157func (m *EmailView) View() string {
158	header := fmt.Sprintf("From: %s | Subject: %s", m.email.From, m.email.Subject)
159	styledHeader := emailHeaderStyle.Width(m.viewport.Width).Render(header)
160
161	var help string
162	if m.focusOnAttachments {
163		help = helpStyle.Render("↑/↓: navigate • enter: download • esc/tab: back to email body")
164	} else {
165		help = helpStyle.Render("r: reply • d: delete • a: archive • tab: focus attachments • esc: back to inbox")
166	}
167
168	var attachmentView string
169	if len(m.email.Attachments) > 0 {
170		var b strings.Builder
171		b.WriteString("Attachments:\n")
172		for i, attachment := range m.email.Attachments {
173			cursor := "  "
174			style := itemStyle
175			if m.focusOnAttachments && i == m.attachmentCursor {
176				cursor = "> "
177				style = selectedItemStyle
178			}
179			b.WriteString(style.Render(fmt.Sprintf("%s%s", cursor, attachment.Filename)))
180			b.WriteString("\n")
181		}
182		attachmentView = attachmentBoxStyle.Render(b.String())
183	}
184
185	return fmt.Sprintf("%s\n%s\n%s\n%s", styledHeader, m.viewport.View(), attachmentView, help)
186}
187
188// GetAccountID returns the account ID for this email
189func (m *EmailView) GetAccountID() string {
190	return m.accountID
191}
192
193func wrapBodyToWidth(body string, width int) string {
194	return BodyStyle.Width(width).Render(body)
195}
196
197// GetEmail returns the email being viewed
198func (m *EmailView) GetEmail() fetcher.Email {
199	return m.email
200}