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