feat: dates in inbox (#267)

Drew Smirnoff created

Change summary

tui/inbox.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 74 insertions(+), 1 deletion(-)

Detailed changes

tui/inbox.go 🔗

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"io"
 	"strings"
+	"time"
 
 	"charm.land/bubbles/v2/key"
 	"charm.land/bubbles/v2/list"
@@ -22,12 +23,15 @@ var (
 	tabBarStyle     = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).PaddingBottom(1).MarginBottom(1)
 )
 
+var dateStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
+
 type item struct {
 	title, desc   string
 	originalIndex int
 	uid           uint32
 	accountID     string
 	accountEmail  string
+	date          time.Time
 }
 
 func (i item) Title() string       { return i.title }
@@ -52,6 +56,36 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
 		str = fmt.Sprintf("%d. [%s] %s", index+1, truncateEmail(i.accountEmail), i.title)
 	}
 
+	// Format and right-align date
+	dateStr := formatRelativeDate(i.date)
+	styledDate := dateStyle.Render(dateStr)
+	dateWidth := lipgloss.Width(styledDate)
+
+	// Truncate the left part to fit within the available width
+	listWidth := m.Width()
+	isSelected := index == m.Index()
+	cursorWidth := 0
+	if isSelected {
+		cursorWidth = 2 // "> " prefix
+	}
+	maxLeft := listWidth - dateWidth - 2 - cursorWidth // 2 for spacing
+	if maxLeft < 10 {
+		maxLeft = 10
+	}
+	if lipgloss.Width(str) > maxLeft {
+		// Truncate with ellipsis
+		for lipgloss.Width(str) > maxLeft-1 && len(str) > 0 {
+			str = str[:len(str)-1]
+		}
+		str += "…"
+	}
+
+	// Pad to push date to the right
+	padding := listWidth - lipgloss.Width(str) - dateWidth - cursorWidth
+	if padding < 1 {
+		padding = 1
+	}
+
 	fn := itemStyle.Render
 	if index == m.Index() {
 		fn = func(s ...string) string {
@@ -59,7 +93,45 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
 		}
 	}
 
-	fmt.Fprint(w, fn(str))
+	fmt.Fprint(w, fn(str+strings.Repeat(" ", padding)+styledDate))
+}
+
+// formatRelativeDate formats a time as relative if within the last week,
+// otherwise as an absolute date.
+func formatRelativeDate(t time.Time) string {
+	if t.IsZero() {
+		return ""
+	}
+	now := time.Now()
+	d := now.Sub(t)
+
+	switch {
+	case d < time.Minute:
+		return "just now"
+	case d < time.Hour:
+		mins := int(d.Minutes())
+		if mins == 1 {
+			return "1 min ago"
+		}
+		return fmt.Sprintf("%d min ago", mins)
+	case d < 24*time.Hour:
+		hours := int(d.Hours())
+		if hours == 1 {
+			return "1 hour ago"
+		}
+		return fmt.Sprintf("%d hours ago", hours)
+	case d < 7*24*time.Hour:
+		days := int(d.Hours() / 24)
+		if days == 1 {
+			return "1 day ago"
+		}
+		return fmt.Sprintf("%d days ago", days)
+	default:
+		if t.Year() == now.Year() {
+			return t.Format("Jan 02")
+		}
+		return t.Format("Jan 02, 2006")
+	}
 }
 
 // truncateEmail shortens an email for display
@@ -205,6 +277,7 @@ func (m *Inbox) updateList() {
 			uid:           email.UID,
 			accountID:     email.AccountID,
 			accountEmail:  accountEmail,
+			date:          email.Date,
 		}
 	}