aboutsummaryrefslogtreecommitdiffstats
path: root/widgets/msglist.go
diff options
context:
space:
mode:
authory0ast <joost@joo.st>2021-11-12 18:12:02 +0100
committerRobin Jarry <robin@jarry.cc>2021-11-13 15:05:59 +0100
commitdc2a2c2dfd6dc327fe40fbf2da922ef6c3d520be (patch)
tree4987160692aca01e27b068cb256d66d373556a52 /widgets/msglist.go
parentc303b953360994966ff657c4e17670853198ecf7 (diff)
downloadaerc-dc2a2c2dfd6dc327fe40fbf2da922ef6c3d520be.tar.gz
messages: allow displaying email threads
Display threads in the message list. For now, only supported by the notmuch backend and on IMAP when the server supports the THREAD extension. Setting threading-enable=true is global and will cause the message list to be empty with maildir:// accounts. Co-authored-by: Kevin Kuehler <keur@xcf.berkeley.edu> Co-authored-by: Reto Brunner <reto@labrat.space> Signed-off-by: Robin Jarry <robin@jarry.cc>
Diffstat (limited to 'widgets/msglist.go')
-rw-r--r--widgets/msglist.go255
1 files changed, 174 insertions, 81 deletions
diff --git a/widgets/msglist.go b/widgets/msglist.go
index 324e14d8..6163d0e5 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -4,7 +4,9 @@ import (
"fmt"
"log"
"math"
+ "strings"
+ sortthread "github.com/emersion/go-imap-sortthread"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
@@ -13,6 +15,7 @@ import (
"git.sr.ht/~rjarry/aerc/lib/format"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/models"
+ "git.sr.ht/~rjarry/aerc/worker/types"
)
type MessageList struct {
@@ -85,95 +88,73 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
needsHeaders []uint32
row int = 0
)
- uids := store.Uids()
-
- for i := len(uids) - 1 - ml.scroll; i >= 0; i-- {
- uid := uids[i]
- msg := store.Messages[uid]
- if row >= ctx.Height() {
- break
- }
-
- if msg == nil {
- needsHeaders = append(needsHeaders, uid)
- ml.spinner.Draw(ctx.Subcontext(0, row, textWidth, 1))
- row += 1
- continue
- }
+ if ml.aerc.SelectedAccount().UiConfig().ThreadingEnabled {
+ threads := store.Threads
+ counter := len(store.Uids())
- confParams := map[config.ContextType]string{
- config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
- config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(),
- }
- if msg.Envelope != nil {
- confParams[config.UI_CONTEXT_SUBJECT] = msg.Envelope.Subject
- }
- uiConfig := ml.conf.GetUiConfig(confParams)
-
- msg_styles := []config.StyleObject{}
- // unread message
- seen := false
- flagged := false
- for _, flag := range msg.Flags {
- switch flag {
- case models.SeenFlag:
- seen = true
- case models.FlaggedFlag:
- flagged = true
+ for i := len(threads) - 1; i >= 0; i-- {
+ var lastSubject string
+ threads[i].Walk(func(t *types.Thread, _ int, currentErr error) error {
+ if currentErr != nil {
+ return currentErr
+ }
+ if t.Hidden || t.Deleted {
+ return nil
+ }
+ counter--
+ if counter > len(store.Uids())-1-ml.scroll {
+ //skip messages which are higher than the viewport
+ return nil
+ }
+ msg := store.Messages[t.Uid]
+ var prefix string
+ var subject string
+ var normalizedSubject string
+ if msg != nil {
+ prefix = threadPrefix(t)
+ if msg.Envelope != nil {
+ subject = msg.Envelope.Subject
+ normalizedSubject, _ = sortthread.GetBaseSubject(subject)
+ }
+ }
+ fmtCtx := format.Ctx{
+ FromAddress: ml.aerc.SelectedAccount().acct.From,
+ AccountName: ml.aerc.SelectedAccount().Name(),
+ MsgInfo: msg,
+ MsgNum: row,
+ MsgIsMarked: store.IsMarked(t.Uid),
+ ThreadPrefix: prefix,
+ ThreadSameSubject: normalizedSubject == lastSubject,
+ }
+ if ml.drawRow(textWidth, ctx, t.Uid, row, &needsHeaders, fmtCtx) {
+ return types.ErrSkipThread
+ }
+ lastSubject = normalizedSubject
+ row++
+ return nil
+ })
+ if row >= ctx.Height() {
+ break
}
}
-
- if seen {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_READ)
- } else {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_UNREAD)
- }
-
- if flagged {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_FLAGGED)
- }
-
- // deleted message
- if _, ok := store.Deleted[msg.Uid]; ok {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_DELETED)
- }
-
- // marked message
- if store.IsMarked(msg.Uid) {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_MARKED)
- }
-
- var style tcell.Style
- // current row
- if row == ml.store.SelectedIndex()-ml.scroll {
- style = uiConfig.GetComposedStyleSelected(config.STYLE_MSGLIST_DEFAULT, msg_styles)
- } else {
- style = uiConfig.GetComposedStyle(config.STYLE_MSGLIST_DEFAULT, msg_styles)
- }
-
- ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
- fmtStr, args, err := format.ParseMessageFormat(
- uiConfig.IndexFormat, uiConfig.TimestampFormat,
- uiConfig.ThisDayTimeFormat,
- uiConfig.ThisWeekTimeFormat,
- uiConfig.ThisYearTimeFormat,
- format.Ctx{
+ } else {
+ uids := store.Uids()
+ for i := len(uids) - 1 - ml.scroll; i >= 0; i-- {
+ uid := uids[i]
+ msg := store.Messages[uid]
+ fmtCtx := format.Ctx{
FromAddress: ml.aerc.SelectedAccount().acct.From,
AccountName: ml.aerc.SelectedAccount().Name(),
MsgInfo: msg,
- MsgNum: i,
+ MsgNum: row,
MsgIsMarked: store.IsMarked(uid),
- })
- if err != nil {
- ctx.Printf(0, row, style, "%v", err)
- } else {
- line := fmt.Sprintf(fmtStr, args...)
- line = runewidth.Truncate(line, textWidth, "…")
- ctx.Printf(0, row, style, "%s", line)
+ }
+ if ml.drawRow(textWidth, ctx, uid, row, &needsHeaders, fmtCtx) {
+ break
+ }
+ row += 1
}
-
- row += 1
}
if needScrollbar {
@@ -181,7 +162,7 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
ml.drawScrollbar(scrollbarCtx, percentVisible)
}
- if len(uids) == 0 {
+ if len(store.Uids()) == 0 {
if store.Sorting {
ml.spinner.Start()
ml.spinner.Draw(ctx)
@@ -199,6 +180,88 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
}
}
+func (ml *MessageList) drawRow(textWidth int, ctx *ui.Context, uid uint32, row int, needsHeaders *[]uint32, fmtCtx format.Ctx) bool {
+ store := ml.store
+ msg := store.Messages[uid]
+
+ if row >= ctx.Height() {
+ return true
+ }
+
+ if msg == nil {
+ *needsHeaders = append(*needsHeaders, uid)
+ ml.spinner.Draw(ctx.Subcontext(0, row, textWidth, 1))
+ return false
+ }
+
+ confParams := map[config.ContextType]string{
+ config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
+ config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(),
+ }
+ if msg.Envelope != nil {
+ confParams[config.UI_CONTEXT_SUBJECT] = msg.Envelope.Subject
+ }
+ uiConfig := ml.conf.GetUiConfig(confParams)
+
+ msg_styles := []config.StyleObject{}
+ // unread message
+ seen := false
+ flagged := false
+ for _, flag := range msg.Flags {
+ switch flag {
+ case models.SeenFlag:
+ seen = true
+ case models.FlaggedFlag:
+ flagged = true
+ }
+ }
+
+ if seen {
+ msg_styles = append(msg_styles, config.STYLE_MSGLIST_READ)
+ } else {
+ msg_styles = append(msg_styles, config.STYLE_MSGLIST_UNREAD)
+ }
+
+ if flagged {
+ msg_styles = append(msg_styles, config.STYLE_MSGLIST_FLAGGED)
+ }
+
+ // deleted message
+ if _, ok := store.Deleted[msg.Uid]; ok {
+ msg_styles = append(msg_styles, config.STYLE_MSGLIST_DELETED)
+ }
+
+ // marked message
+ if store.IsMarked(msg.Uid) {
+ msg_styles = append(msg_styles, config.STYLE_MSGLIST_MARKED)
+ }
+
+ var style tcell.Style
+ // current row
+ if row == ml.store.SelectedIndex()-ml.scroll {
+ style = uiConfig.GetComposedStyleSelected(config.STYLE_MSGLIST_DEFAULT, msg_styles)
+ } else {
+ style = uiConfig.GetComposedStyle(config.STYLE_MSGLIST_DEFAULT, msg_styles)
+ }
+
+ ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
+ fmtStr, args, err := format.ParseMessageFormat(
+ uiConfig.IndexFormat, uiConfig.TimestampFormat,
+ uiConfig.ThisDayTimeFormat,
+ uiConfig.ThisWeekTimeFormat,
+ uiConfig.ThisYearTimeFormat,
+ fmtCtx)
+ if err != nil {
+ ctx.Printf(0, row, style, "%v", err)
+ } else {
+ line := fmt.Sprintf(fmtStr, args...)
+ line = runewidth.Truncate(line, textWidth, "…")
+ ctx.Printf(0, row, style, "%s", line)
+ }
+
+ return false
+}
+
func (ml *MessageList) drawScrollbar(ctx *ui.Context, percentVisible float64) {
gutterStyle := tcell.StyleDefault
pillStyle := tcell.StyleDefault.Reverse(true)
@@ -375,3 +438,33 @@ func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg)
}
+
+func threadPrefix(t *types.Thread) string {
+ var arrow string
+ if t.Parent != nil {
+ if t.NextSibling != nil {
+ arrow = "├─>"
+ } else {
+ arrow = "└─>"
+ }
+ }
+ var prefix []string
+ for n := t; n.Parent != nil; n = n.Parent {
+ if n.Parent.NextSibling != nil {
+ prefix = append(prefix, "│ ")
+ } else {
+ prefix = append(prefix, " ")
+ }
+ }
+ // prefix is now in a reverse order (inside --> outside), so turn it
+ for i, j := 0, len(prefix)-1; i < j; i, j = i+1, j-1 {
+ prefix[i], prefix[j] = prefix[j], prefix[i]
+ }
+
+ // we don't want to indent the first child, hence we strip that level
+ if len(prefix) > 0 {
+ prefix = prefix[1:]
+ }
+ ps := strings.Join(prefix, "")
+ return fmt.Sprintf("%v%v", ps, arrow)
+}