~sircmpwn/aerc

[WIP] Start adding thread support v2 PROPOSED

Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/aerc/patches/8864/mbox | git am -3
Learn more about email & git

[PATCH v2 1/9] [WIP] Start adding thread support Export this patch

* Add threading-enabled config option
* Add DirectoryThreaded and FetchDirectoryThreaded types to control path
* Add generic thread type for all backends to use

Signed-off-by: Kevin Kuehler <keur@xcf.berkeley.edu>
---
 config/aerc.conf.in      |  6 ++++++
 config/config.go         |  1 +
 lib/msgstore.go          | 18 +++++++++++++++---
 widgets/account.go       |  5 +++++
 worker/types/messages.go | 10 ++++++++++
 worker/types/thread.go   |  6 ++++++
 6 files changed, 43 insertions(+), 3 deletions(-)
 create mode 100644 worker/types/thread.go

diff --git a/config/aerc.conf.in b/config/aerc.conf.in
index ec89ff7..362dd51 100644
--- a/config/aerc.conf.in
+++ b/config/aerc.conf.in
@@ -32,6 +32,12 @@ empty-message=(no messages)
 # Default: (no folders)
 empty-dirlist=(no folders)

+#
+# Enable threading in the ui
+#
+# Default: false
+threading-enabled=false
+
 # Enable mouse events in the ui, e.g. clicking and scrolling with the mousewheel
 #
 # Default: false
diff --git a/config/config.go b/config/config.go
index 133a7f4..4170b79 100644
--- a/config/config.go
+++ b/config/config.go
@@ -32,6 +32,7 @@ type UIConfig struct {
 	EmptyMessage        string   `ini:"empty-message"`
 	EmptyDirlist        string   `ini:"empty-dirlist"`
 	MouseEnabled        bool     `ini:"mouse-enabled"`
+	ThreadingEnabled    bool     `ini:"threading-enabled"`
 	NewMessageBell      bool     `ini:"new-message-bell"`
 	Spinner             string   `ini:"spinner"`
 	SpinnerDelimiter    string   `ini:"spinner-delimiter"`
diff --git a/lib/msgstore.go b/lib/msgstore.go
index 56d9eda..c0e4136 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -21,6 +21,9 @@ type MessageStore struct {
 	bodyCallbacks   map[uint32][]func(io.Reader)
 	headerCallbacks map[uint32][]func(*types.MessageInfo)

+	// If set, messages in the mailbox will be threaded
+	thread bool
+
 	// Search/filter results
 	results     []uint32
 	resultIndex int
@@ -42,6 +45,7 @@ type MessageStore struct {
 func NewMessageStore(worker *types.Worker,
 	dirInfo *models.DirectoryInfo,
 	defaultSortCriteria []*types.SortCriterion,
+	thread bool,
 	triggerNewEmail func(*models.MessageInfo),
 	triggerDirectoryChange func()) *MessageStore {

@@ -53,6 +57,8 @@ func NewMessageStore(worker *types.Worker,
 		bodyCallbacks:   make(map[uint32][]func(io.Reader)),
 		headerCallbacks: make(map[uint32][]func(*types.MessageInfo)),

+		thread: thread,
+
 		defaultSortCriteria: defaultSortCriteria,

 		pendingBodies:  make(map[uint32]interface{}),
@@ -157,9 +163,15 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 	switch msg := msg.(type) {
 	case *types.DirectoryInfo:
 		store.DirInfo = *msg.Info
-		store.worker.PostAction(&types.FetchDirectoryContents{
-			SortCriteria: store.defaultSortCriteria,
-		}, nil)
+		if store.thread {
+			store.worker.PostAction(&types.FetchDirectoryThreaded{
+				SortCriteria: store.defaultSortCriteria,
+			}, nil)
+		} else {
+			store.worker.PostAction(&types.FetchDirectoryContents{
+				SortCriteria: store.defaultSortCriteria,
+			}, nil)
+		}
 		update = true
 	case *types.DirectoryContents:
 		newMap := make(map[uint32]*models.MessageInfo)
diff --git a/widgets/account.go b/widgets/account.go
index 4e8dd17..ebc321d 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -220,6 +220,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
 		} else {
 			store = lib.NewMessageStore(acct.worker, msg.Info,
 				acct.getSortCriteria(),
+				acct.conf.Ui.ThreadingEnabled,
 				func(msg *models.MessageInfo) {
 					acct.conf.Triggers.ExecNewEmail(acct.acct,
 						acct.conf, msg)
@@ -238,6 +239,10 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
 		if store, ok := acct.dirlist.SelectedMsgStore(); ok {
 			store.Update(msg)
 		}
+	case *types.DirectoryThreaded:
+		if store, ok := acct.dirlist.SelectedMsgStore(); ok {
+			store.Update(msg)
+		}
 	case *types.FullMessage:
 		if store, ok := acct.dirlist.SelectedMsgStore(); ok {
 			store.Update(msg)
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 3539139..0b9dc6e 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -81,6 +81,11 @@ type FetchDirectoryContents struct {
 	SortCriteria []*SortCriterion
 }

+type FetchDirectoryThreaded struct {
+	Message
+	SortCriteria []*SortCriterion
+}
+
 type SearchDirectory struct {
 	Message
 	Argv []string
@@ -152,6 +157,11 @@ type DirectoryContents struct {
 	Uids []uint32
 }

+type DirectoryThreaded struct {
+	Message
+	Threads []*Thread
+}
+
 type SearchResults struct {
 	Message
 	Uids []uint32
diff --git a/worker/types/thread.go b/worker/types/thread.go
new file mode 100644
index 0000000..265438f
--- /dev/null
+++ b/worker/types/thread.go
@@ -0,0 +1,6 @@
+package types
+
+type Thread struct {
+	Uid      uint32
+	Children []*Thread
+}
--
2.23.0

[PATCH v2 2/9] [WIP] worker/imap: Add threading extension Export this patch

* Import the go-imap-sortthread library
* Add sortthread client to imapClient in worker
* Add handleDirectoryThreaded, which uses the go-imap-sortthread, and
  converts the results to the aerc thread type

Signed-off-by: Kevin Kuehler <keur@xcf.berkeley.edu>
---
 go.mod                |  3 ++-
 go.sum                |  4 ++++
 worker/imap/open.go   | 45 +++++++++++++++++++++++++++++++++++++++++++
 worker/imap/worker.go |  6 +++++-
 4 files changed, 56 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index aeb7f8c..52a0309 100644
--- a/go.mod
+++ b/go.mod
@@ -9,9 +9,10 @@ require (
 	github.com/ddevault/go-libvterm v0.0.0-20190526194226-b7d861da3810
 	github.com/emersion/go-imap v1.0.0
 	github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e
+	github.com/emersion/go-imap-sortthread v1.0.1-0.20191002194849-97d602f80823
 	github.com/emersion/go-maildir v0.0.0-20190727102040-941194b0ac70
 	github.com/emersion/go-message v0.10.7
-	github.com/emersion/go-sasl v0.0.0-20190704090222-36b50694675c
+	github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e
 	github.com/emersion/go-smtp v0.11.2
 	github.com/fsnotify/fsnotify v1.4.7
 	github.com/gdamore/tcell v1.1.5-0.20190724020331-84b54971b46c
diff --git a/go.sum b/go.sum
index 2749ac5..8f08a7a 100644
--- a/go.sum
+++ b/go.sum
@@ -19,6 +19,8 @@ github.com/emersion/go-imap v1.0.0 h1:/7HHNiSOk13DErenBZaQfTBmUy+quc6X7s3RNnuVtU
 github.com/emersion/go-imap v1.0.0/go.mod h1:MEiDDwwQFcZ+L45Pa68jNGv0qU9kbW+SJzwDpvSfX1s=
 github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e h1:L7bswVJZcf2YHofgom49oFRwVqmBj/qZqDy9/SJpZMY=
 github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
+github.com/emersion/go-imap-sortthread v1.0.1-0.20191002194849-97d602f80823 h1:E0Uc67DpLgG/nt8NhEqQ77TNGq3zDH1uvi4obW3SAyU=
+github.com/emersion/go-imap-sortthread v1.0.1-0.20191002194849-97d602f80823/go.mod h1:opHOzblOHZKQM1JEy+GPk1217giNLa7kleyWTN06qnc=
 github.com/emersion/go-maildir v0.0.0-20190727102040-941194b0ac70 h1:aUiPu6/iCjcsnNe/WkhsnMOq7vPmkYo9kFaMX5FiNZU=
 github.com/emersion/go-maildir v0.0.0-20190727102040-941194b0ac70/go.mod h1:I2j27lND/SRLgxROe50Vam81MSaqPFvJ0OHNnDZ7n84=
 github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
@@ -28,6 +30,8 @@ github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317 h1:tYZxAY8nu3JJQK
 github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
 github.com/emersion/go-sasl v0.0.0-20190704090222-36b50694675c h1:Spm8jy+jWYG/Dn6ygbq/LBW/6M27kg59GK+FkKjexuw=
 github.com/emersion/go-sasl v0.0.0-20190704090222-36b50694675c/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
+github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q=
+github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
 github.com/emersion/go-smtp v0.11.2 h1:5PO2Kwsx+HXuytntCfMvcworC/iq45TPGkwjnaBZFSg=
 github.com/emersion/go-smtp v0.11.2/go.mod h1:byi9Y32SuKwjTJt9DO2tTWYjtF3lEh154tE1AcaJQSY=
 github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
diff --git a/worker/imap/open.go b/worker/imap/open.go
index 452c309..bdb794b 100644
--- a/worker/imap/open.go
+++ b/worker/imap/open.go
@@ -2,6 +2,7 @@ package imap
 
 import (
 	"github.com/emersion/go-imap"
+	sortthread "github.com/emersion/go-imap-sortthread"
 
 	"git.sr.ht/~sircmpwn/aerc/worker/types"
 )
@@ -48,3 +49,47 @@ func (imapw *IMAPWorker) handleFetchDirectoryContents(
 		imapw.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil)
 	}
 }
+
+func (imapw *IMAPWorker) handleDirectoryThreaded(
+	msg *types.FetchDirectoryThreaded) {
+	imapw.worker.Logger.Printf("Fetching threaded UID list")
+
+	seqSet := &imap.SeqSet{}
+	seqSet.AddRange(1, imapw.selected.Messages)
+	threads, err := imapw.client.tc.UidThread(sortthread.References,
+		&imap.SearchCriteria{SeqNum: seqSet})
+	if err != nil {
+		imapw.worker.PostMessage(&types.Error{
+			Message: types.RespondTo(msg),
+			Error:   err,
+		}, nil)
+	} else {
+		aercThreads, count := convertThreads(threads)
+		imapw.seqMap = make([]uint32, count)
+		imapw.worker.PostMessage(&types.DirectoryThreaded{
+			Message: types.RespondTo(msg),
+			Threads: aercThreads,
+		}, nil)
+		imapw.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil)
+	}
+}
+
+// This sucks... TODO: find a better way to do this.
+func convertThreads(threads []*sortthread.Thread) ([]*types.Thread, int) {
+	if threads == nil {
+		return nil, 0
+	}
+	conv := make([]*types.Thread, len(threads))
+	count := 0
+
+	for i := 0; i < len(threads); i++ {
+		t := threads[i]
+		children, childCount := convertThreads(t.Children)
+		conv[i] = &types.Thread{
+			Uid:      t.Id,
+			Children: children,
+		}
+		count += childCount + 1
+	}
+	return conv, count
+}
diff --git a/worker/imap/worker.go b/worker/imap/worker.go
index 4d3e51c..1ba3774 100644
--- a/worker/imap/worker.go
+++ b/worker/imap/worker.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/emersion/go-imap"
 	idle "github.com/emersion/go-imap-idle"
+	sortthread "github.com/emersion/go-imap-sortthread"
 	"github.com/emersion/go-imap/client"
 	"golang.org/x/oauth2"
 
@@ -26,6 +27,7 @@ var errUnsupported = fmt.Errorf("unsupported command")
 
 type imapClient struct {
 	*client.Client
+	tc   *sortthread.ThreadClient
 	idle *idle.IdleClient
 }
 
@@ -153,7 +155,7 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
 		}
 
 		c.Updates = w.updates
-		w.client = &imapClient{c, idle.NewClient(c)}
+		w.client = &imapClient{c, sortthread.NewThreadClient(c), idle.NewClient(c)}
 		w.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil)
 	case *types.ListDirectories:
 		w.handleListDirectories(msg)
@@ -161,6 +163,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
 		w.handleOpenDirectory(msg)
 	case *types.FetchDirectoryContents:
 		w.handleFetchDirectoryContents(msg)
+	case *types.FetchDirectoryThreaded:
+		w.handleDirectoryThreaded(msg)
 	case *types.CreateDirectory:
 		w.handleCreateDirectory(msg)
 	case *types.FetchMessageHeaders:
-- 
2.23.0

[PATCH v2 3/9] [WIP] widgets/msglist: Draw each row in a function Export this patch

This function can be reused by the threading code.

Signed-off-by: Kevin Kuehler <keur@xcf.berkeley.edu>
---
 widgets/msglist.go | 98 +++++++++++++++++++++++++---------------------
 1 file changed, 54 insertions(+), 44 deletions(-)

diff --git a/widgets/msglist.go b/widgets/msglist.go
index 5c2d2f4..ac3d6cc 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -71,52 +71,9 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
 
 	for i := len(uids) - 1 - ml.scroll; i >= 0; i-- {
 		uid := uids[i]
-		msg := store.Messages[uid]
-
-		if row >= ctx.Height() {
+		if ml.drawRow(ctx, uid, i, row, &needsHeaders) {
 			break
 		}
-
-		if msg == nil {
-			needsHeaders = append(needsHeaders, uid)
-			ml.spinner.Draw(ctx.Subcontext(0, row, ctx.Width(), 1))
-			row += 1
-			continue
-		}
-
-		style := tcell.StyleDefault
-
-		// current row
-		if row == ml.store.SelectedIndex()-ml.scroll {
-			style = style.Reverse(true)
-		}
-		// deleted message
-		if _, ok := store.Deleted[msg.Uid]; ok {
-			style = style.Foreground(tcell.ColorGray)
-		}
-		// unread message
-		seen := false
-		for _, flag := range msg.Flags {
-			if flag == models.SeenFlag {
-				seen = true
-			}
-		}
-		if !seen {
-			style = style.Bold(true)
-		}
-
-		ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
-		fmtStr, args, err := format.ParseMessageFormat(
-			ml.conf.Ui.IndexFormat,
-			ml.conf.Ui.TimestampFormat, "", i, msg)
-		if err != nil {
-			ctx.Printf(0, row, style, "%v", err)
-		} else {
-			line := fmt.Sprintf(fmtStr, args...)
-			line = runewidth.Truncate(line, ctx.Width(), "…")
-			ctx.Printf(0, row, style, "%s", line)
-		}
-
 		row += 1
 	}
 
@@ -132,6 +89,59 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
 	}
 }
 
+// Draw one message.
+//
+// Return: If true, don't fetch any more messages
+func (ml *MessageList) drawRow(ctx *ui.Context, uid uint32, number int, row int, needsHeaders *[]uint32) 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, ctx.Width(), 1))
+		return false
+	}
+
+	style := tcell.StyleDefault
+
+	// current row
+	if row == ml.store.SelectedIndex()-ml.scroll {
+		style = style.Reverse(true)
+	}
+	// deleted message
+	if _, ok := store.Deleted[msg.Uid]; ok {
+		style = style.Foreground(tcell.ColorGray)
+	}
+	// unread message
+	seen := false
+	for _, flag := range msg.Flags {
+		if flag == models.SeenFlag {
+			seen = true
+		}
+	}
+	if !seen {
+		style = style.Bold(true)
+	}
+
+	ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
+	fmtStr, args, err := format.ParseMessageFormat(
+		ml.conf.Ui.IndexFormat,
+		ml.conf.Ui.TimestampFormat, "", number, msg)
+	if err != nil {
+		ctx.Printf(0, row, style, "%v", err)
+	} else {
+		line := fmt.Sprintf(fmtStr, args...)
+		line = runewidth.Truncate(line, ctx.Width(), "…")
+		ctx.Printf(0, row, style, "%s", line)
+	}
+
+	return false
+}
+
 func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
 	switch event := event.(type) {
 	case *tcell.EventMouse:
-- 
2.23.0

[PATCH v2 4/9] [WIP] Add FormatThread function Export this patch

FormatThread performs dfs on a thread tree. For every node in the tree,
a callback function is called with a thread and a format string for
that thread. The format string is to be used when displaying the full
thread tree.

Signed-off-by: Kevin Kuehler <keur@xcf.berkeley.edu>
---
 worker/types/thread.go | 40 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/worker/types/thread.go b/worker/types/thread.go
index 265438f..a51a5c4 100644
--- a/worker/types/thread.go
+++ b/worker/types/thread.go
@@ -4,3 +4,43 @@ type Thread struct {
 	Uid      uint32
 	Children []*Thread
 }
+
+func (t *Thread) FormatThread(cb func(*Thread, []rune) bool) {
+	cb(t, []rune{})
+	walkThreads(t.Children, []rune{}, cb)
+}
+
+func walkThreads(threads []*Thread, threadFmt []rune,
+	cb func(*Thread, []rune) bool) {
+	if threads == nil {
+		return
+	}
+
+	var (
+		indent          []rune = []rune{'\u0020', '\u0020'}
+		indentConnected []rune = []rune{'\u2502', '\u0020'}
+		arrow           []rune = []rune{'\u251c', '\u2500', '\u003e'}
+		arrowConnected  []rune = []rune{'\u2514', '\u2500', '\u003e'}
+
+		threadPrefix          []rune = append(threadFmt, arrow...)
+		threadPrefixConnected []rune = append(threadFmt, arrowConnected...)
+		nextThread            []rune = append(threadFmt, indent...)
+		nextThreadConnected   []rune = append(threadFmt, indentConnected...)
+	)
+
+	for i := len(threads) - 1; i >= 0; i-- {
+		t := threads[i]
+		if i > 0 && len(threads) > 1 {
+			if cb(t, threadPrefix) {
+				return
+			}
+			walkThreads(t.Children, nextThreadConnected, cb)
+		} else {
+			if cb(t, threadPrefixConnected) {
+				return
+			}
+			walkThreads(t.Children, nextThread, cb)
+		}
+	}
+
+}
-- 
2.23.0

[PATCH v2 5/9] [WIP] lib/msgstore: Handle DirectoryThreaded msg Export this patch

This method is called after a worker fetches a threaded directory
contents from the backend. We iterate over the threads in the same order
that they will be printed in the msglist.

Signed-off-by: Kevin Kuehler <keur@xcf.berkeley.edu>
---
 lib/msgstore.go | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/lib/msgstore.go b/lib/msgstore.go
index c0e4136..c2361dc 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -15,7 +15,8 @@ type MessageStore struct {
 	DirInfo  models.DirectoryInfo
 	Messages map[uint32]*models.MessageInfo
 	// Ordered list of known UIDs
-	uids []uint32
+	uids    []uint32
+	Threads []*types.Thread
 
 	selected        int
 	bodyCallbacks   map[uint32][]func(io.Reader)
@@ -173,6 +174,27 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 			}, nil)
 		}
 		update = true
+	case *types.DirectoryThreaded:
+		var uids []uint32
+		newMap := make(map[uint32]*models.MessageInfo)
+
+		for i := len(msg.Threads) - 1; i >= 0; i-- {
+			msg.Threads[i].FormatThread(func(t *types.Thread, x string) bool {
+				uid := t.Uid
+				uids = append([]uint32{uid}, uids...)
+				if msg, ok := store.Messages[uid]; ok {
+					newMap[uid] = msg
+				} else {
+					newMap[uid] = nil
+					directoryChange = true
+				}
+				return false
+			})
+		}
+		store.Messages = newMap
+		store.uids = uids
+		store.Threads = msg.Threads
+		update = true
 	case *types.DirectoryContents:
 		newMap := make(map[uint32]*models.MessageInfo)
 		for _, uid := range msg.Uids {
-- 
2.23.0

[PATCH v2 6/9] [WIP] Add threading control path to msglist.Draw() Export this patch

Signed-off-by: Kevin Kuehler <keur@xcf.berkeley.edu>
---
 config/triggers.go   |  2 +-
 lib/format/format.go |  6 +++---
 lib/msgstore.go      |  2 +-
 widgets/msglist.go   | 29 ++++++++++++++++++++++-------
 4 files changed, 27 insertions(+), 12 deletions(-)

diff --git a/config/triggers.go b/config/triggers.go
index d31f267..0e1f030 100644
--- a/config/triggers.go
+++ b/config/triggers.go
@@ -37,7 +37,7 @@ func (trig *TriggersConfig) ExecNewEmail(account *AccountConfig,
 	err := trig.ExecTrigger(trig.NewEmail,
 		func(part string) (string, error) {
 			formatstr, args, err := format.ParseMessageFormat(part,
-				conf.Ui.TimestampFormat, account.Name, 0, msg)
+				conf.Ui.TimestampFormat, account.Name, 0, msg, "") // TODO: check with jeffas how to handle this
 			if err != nil {
 				return "", err
 			}
diff --git a/lib/format/format.go b/lib/format/format.go
index b403f2d..c71ae93 100644
--- a/lib/format/format.go
+++ b/lib/format/format.go
@@ -10,8 +10,8 @@ import (
 )
 
 func ParseMessageFormat(format string, timestampformat string,
-	accountName string, number int, msg *models.MessageInfo) (string,
-	[]interface{}, error) {
+	accountName string, number int, msg *models.MessageInfo,
+	subjectThread string) (string, []interface{}, error) {
 	retval := make([]byte, 0, len(format))
 	var args []interface{}
 
@@ -147,7 +147,7 @@ func ParseMessageFormat(format string, timestampformat string,
 			args = append(args, addrs)
 		case 's':
 			retval = append(retval, 's')
-			args = append(args, msg.Envelope.Subject)
+			args = append(args, subjectThread+msg.Envelope.Subject)
 		case 't':
 			if len(msg.Envelope.To) == 0 {
 				return "", nil,
diff --git a/lib/msgstore.go b/lib/msgstore.go
index c2361dc..e2e968b 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -179,7 +179,7 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 		newMap := make(map[uint32]*models.MessageInfo)
 
 		for i := len(msg.Threads) - 1; i >= 0; i-- {
-			msg.Threads[i].FormatThread(func(t *types.Thread, x string) bool {
+			msg.Threads[i].FormatThread(func(t *types.Thread, x []rune) bool {
 				uid := t.Uid
 				uids = append([]uint32{uid}, uids...)
 				if msg, ok := store.Messages[uid]; ok {
diff --git a/widgets/msglist.go b/widgets/msglist.go
index ac3d6cc..228477d 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -12,6 +12,7 @@ import (
 	"git.sr.ht/~sircmpwn/aerc/lib/format"
 	"git.sr.ht/~sircmpwn/aerc/lib/ui"
 	"git.sr.ht/~sircmpwn/aerc/models"
+	"git.sr.ht/~sircmpwn/aerc/worker/types"
 )
 
 type MessageList struct {
@@ -69,12 +70,26 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
 	)
 	uids := store.Uids()
 
-	for i := len(uids) - 1 - ml.scroll; i >= 0; i-- {
-		uid := uids[i]
-		if ml.drawRow(ctx, uid, i, row, &needsHeaders) {
-			break
+	if ml.conf.Ui.ThreadingEnabled {
+		threads := store.Threads
+
+		for i := len(threads) - 1; i >= 0; i-- {
+			threads[i].FormatThread(func(thread *types.Thread, threadFmt []rune) bool {
+				if ml.drawRow(ctx, thread.Uid, row, row, &needsHeaders, string(threadFmt)) {
+					return true
+				}
+				row += 1
+				return false
+			})
+		}
+	} else {
+		for i := len(uids) - 1 - ml.scroll; i >= 0; i-- {
+			uid := uids[i]
+			if ml.drawRow(ctx, uid, i, row, &needsHeaders, "") {
+				break
+			}
+			row += 1
 		}
-		row += 1
 	}
 
 	if len(uids) == 0 {
@@ -92,7 +107,7 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
 // Draw one message.
 //
 // Return: If true, don't fetch any more messages
-func (ml *MessageList) drawRow(ctx *ui.Context, uid uint32, number int, row int, needsHeaders *[]uint32) bool {
+func (ml *MessageList) drawRow(ctx *ui.Context, uid uint32, number int, row int, needsHeaders *[]uint32, subjectThread string) bool {
 	store := ml.store
 	msg := store.Messages[uid]
 
@@ -130,7 +145,7 @@ func (ml *MessageList) drawRow(ctx *ui.Context, uid uint32, number int, row int,
 	ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
 	fmtStr, args, err := format.ParseMessageFormat(
 		ml.conf.Ui.IndexFormat,
-		ml.conf.Ui.TimestampFormat, "", number, msg)
+		ml.conf.Ui.TimestampFormat, "", number, msg, subjectThread)
 	if err != nil {
 		ctx.Printf(0, row, style, "%v", err)
 	} else {
-- 
2.23.0

[PATCH v2 7/9] [WIP] Rework threading and add REFERENCES Export this patch

* Implement a simplified version of the REFERENCES algorithm
* Remove FormatThreads function
* Instead of acting on all threads, handle each thread independently

Signed-off-by: Kevin Kuehler <keur@xcf.berkeley.edu>
---
 lib/msgstore.go          | 196 +++++++++++++++++++++++++++++++++------
 widgets/msglist.go       |  20 ++--
 worker/imap/open.go      |  21 +++--
 worker/types/messages.go |   2 +-
 worker/types/thread.go   | 116 ++++++++++++++++-------
 5 files changed, 278 insertions(+), 77 deletions(-)

diff --git a/lib/msgstore.go b/lib/msgstore.go
index e2e968b..ccde2c2 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -1,12 +1,15 @@
 package lib
 
 import (
+	"fmt"
 	"io"
+	"sort"
 	"time"
 
-	"git.sr.ht/~sircmpwn/aerc/lib/sort"
+	aercSort "git.sr.ht/~sircmpwn/aerc/lib/sort"
 	"git.sr.ht/~sircmpwn/aerc/models"
 	"git.sr.ht/~sircmpwn/aerc/worker/types"
+	"github.com/emersion/go-message"
 )
 
 // Accesses to fields must be guarded by MessageStore.Lock/Unlock
@@ -15,15 +18,17 @@ type MessageStore struct {
 	DirInfo  models.DirectoryInfo
 	Messages map[uint32]*models.MessageInfo
 	// Ordered list of known UIDs
-	uids    []uint32
-	Threads []*types.Thread
+	uids []uint32
 
 	selected        int
 	bodyCallbacks   map[uint32][]func(io.Reader)
 	headerCallbacks map[uint32][]func(*types.MessageInfo)
 
 	// If set, messages in the mailbox will be threaded
-	thread bool
+	ThreadRoot  *types.Thread
+	FlatThreads []*types.Thread
+	thread      bool
+	threadRefs  map[string]*types.Thread
 
 	// Search/filter results
 	results     []uint32
@@ -57,8 +62,9 @@ func NewMessageStore(worker *types.Worker,
 		selected:        0,
 		bodyCallbacks:   make(map[uint32][]func(io.Reader)),
 		headerCallbacks: make(map[uint32][]func(*types.MessageInfo)),
-
-		thread: thread,
+		ThreadRoot:      &types.Thread{Uid: 0, Dummy: true},
+		thread:          thread,
+		threadRefs:      make(map[string]*types.Thread),
 
 		defaultSortCriteria: defaultSortCriteria,
 
@@ -175,36 +181,47 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 		}
 		update = true
 	case *types.DirectoryThreaded:
-		var uids []uint32
+		var (
+			uids      []uint32
+			flattened []*types.Thread
+		)
 		newMap := make(map[uint32]*models.MessageInfo)
 
-		for i := len(msg.Threads) - 1; i >= 0; i-- {
-			msg.Threads[i].FormatThread(func(t *types.Thread, x []rune) bool {
-				uid := t.Uid
-				uids = append([]uint32{uid}, uids...)
-				if msg, ok := store.Messages[uid]; ok {
-					newMap[uid] = msg
-				} else {
-					newMap[uid] = nil
-					directoryChange = true
-				}
-				return false
-			})
-		}
+		msg.ThreadRoot.Traverse(false, func(t *types.Thread) bool {
+			uid := t.Uid
+			uids = append([]uint32{uid}, uids...)
+			flattened = append([]*types.Thread{t}, flattened...)
+			if msg, ok := store.Messages[uid]; ok {
+				newMap[uid] = msg
+			} else {
+				newMap[uid] = nil
+				directoryChange = true
+			}
+			return false
+		})
 		store.Messages = newMap
 		store.uids = uids
-		store.Threads = msg.Threads
+		store.FlatThreads = flattened
+		store.ThreadRoot = msg.ThreadRoot
 		update = true
 	case *types.DirectoryContents:
+		var needsHeaders []uint32
 		newMap := make(map[uint32]*models.MessageInfo)
 		for _, uid := range msg.Uids {
 			if msg, ok := store.Messages[uid]; ok {
 				newMap[uid] = msg
 			} else {
 				newMap[uid] = nil
+				needsHeaders = append(needsHeaders, uid)
 				directoryChange = true
 			}
 		}
+		if store.thread {
+			// We need the headers to perform references. Grab them all for
+			// now. We can probably be smarter here, but let's get something
+			// working first.
+			store.FetchHeaders(needsHeaders, nil)
+		}
 		store.Messages = newMap
 		store.uids = msg.Uids
 		update = true
@@ -234,6 +251,9 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 				}
 			}
 		}
+		if store.thread {
+			store.threadMessage(msg.Info)
+		}
 		update = true
 	case *types.FullMessage:
 		if _, ok := store.pendingBodies[msg.Content.Uid]; ok {
@@ -248,19 +268,49 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 	case *types.MessagesDeleted:
 		toDelete := make(map[uint32]interface{})
 		for _, uid := range msg.Uids {
+			if store.thread {
+				if needle := store.ThreadRoot.Find(uid); needle != nil {
+					_msg := store.Messages[uid]
+					delete(store.threadRefs, _msg.Envelope.MessageId)
+					needle.Dummy = true
+				}
+			}
 			toDelete[uid] = nil
 			delete(store.Messages, uid)
 			delete(store.Deleted, uid)
 		}
 		uids := make([]uint32, len(store.uids)-len(msg.Uids))
-		j := 0
-		for _, uid := range store.uids {
-			if _, deleted := toDelete[uid]; !deleted && j < len(uids) {
-				uids[j] = uid
-				j += 1
+		if store.thread {
+			flattened := make([]*types.Thread, len(store.FlatThreads)-len(msg.Uids))
+			j := 0
+			for _, uid := range store.uids {
+				if _, deleted := toDelete[uid]; !deleted && j < len(uids) {
+					uids[j] = uid
+					j += 1
+				}
+			}
+			j = 0
+			for _, t := range store.FlatThreads {
+				uid := t.Uid
+				if _, deleted := toDelete[uid]; !deleted && j < len(flattened) {
+					flattened[j] = t
+					j += 1
+				}
 			}
+			fmt.Printf("DELETE UID: prev: %d, new: %d\n", len(store.uids), len(uids))
+			fmt.Printf("DELETE FLAT: prev: %d, new: %d\n", len(store.FlatThreads), len(flattened))
+			store.uids = uids
+			store.FlatThreads = flattened
+		} else {
+			j := 0
+			for _, uid := range store.uids {
+				if _, deleted := toDelete[uid]; !deleted && j < len(uids) {
+					uids[j] = uid
+					j += 1
+				}
+			}
+			store.uids = uids
 		}
-		store.uids = uids
 		update = true
 	}
 
@@ -273,6 +323,96 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 	}
 }
 
+func (store *MessageStore) threadMessage(msg *models.MessageInfo) {
+	var (
+		fields message.HeaderFields
+
+		childThread *types.Thread
+		irt         *types.Thread
+		roots       []*types.Thread
+	)
+	if msg.Envelope == nil {
+		return
+	}
+
+	newRefs := make(map[string]*types.Thread)
+
+	if thread, ok := store.threadRefs[msg.Envelope.MessageId]; ok {
+		// Are we in the references table as someone else's parent?
+		thread.Dummy = false
+		thread.Uid = msg.Uid
+		childThread = thread
+	} else {
+		// Then we create a new thread
+		childThread = &types.Thread{Uid: msg.Uid}
+	}
+
+	newRefs[msg.Envelope.MessageId] = childThread
+
+	fields = msg.RFC822Headers.FieldsByKey("In-Reply-To")
+	if fields.Next() {
+		inReplyHeader, err := fields.Text()
+		if err != nil {
+			return
+		}
+		if p, ok := store.threadRefs[inReplyHeader]; ok {
+			irt = p
+		} else {
+			irt = &types.Thread{Uid: 0, Dummy: true}
+		}
+		childThread.Parent = irt
+		irt.Children = append(irt.Children, childThread)
+		newRefs[inReplyHeader] = irt
+	}
+
+	for r, t := range store.threadRefs {
+		if _, ok := newRefs[r]; !ok {
+			newRefs[r] = t
+		}
+	}
+
+	for _, t := range newRefs {
+		if t.Parent == nil || t.Parent == store.ThreadRoot {
+			roots = append(roots, t)
+			t.Parent = store.ThreadRoot
+		}
+	}
+	store.ThreadRoot.Children = roots
+
+	var (
+		uids      []uint32
+		flattened []*types.Thread
+	)
+
+	if len(store.pendingHeaders) == 0 {
+		// Sort the root of the tree
+		children := store.ThreadRoot.Children
+		sort.Slice(children, func(i, j int) bool {
+			ci, cj := children[i], children[j]
+			if ci.Dummy {
+				ci = ci.Children[0]
+			}
+			if cj.Dummy {
+				cj = cj.Children[0]
+			}
+			mi, mj := store.Messages[ci.Uid], store.Messages[cj.Uid]
+			return mi.InternalDate.After(mj.InternalDate)
+		})
+
+		// Linearize tree
+		store.ThreadRoot.Traverse(false, func(t *types.Thread) bool {
+			uid := t.Uid
+			uids = append([]uint32{uid}, uids...)
+			flattened = append(flattened, t)
+			return false
+		})
+	}
+
+	store.FlatThreads = flattened
+	store.threadRefs = newRefs
+	store.uids = uids
+}
+
 func (store *MessageStore) OnUpdate(fn func(store *MessageStore)) {
 	store.onUpdate = fn
 }
@@ -418,7 +558,7 @@ func (store *MessageStore) Search(args []string, cb func([]uint32)) {
 	}, func(msg types.WorkerMessage) {
 		switch msg := msg.(type) {
 		case *types.SearchResults:
-			sort.SortBy(msg.Uids, store.uids)
+			aercSort.SortBy(msg.Uids, store.uids)
 			cb(msg.Uids)
 		}
 	})
diff --git a/widgets/msglist.go b/widgets/msglist.go
index 228477d..df0366d 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -12,7 +12,7 @@ import (
 	"git.sr.ht/~sircmpwn/aerc/lib/format"
 	"git.sr.ht/~sircmpwn/aerc/lib/ui"
 	"git.sr.ht/~sircmpwn/aerc/models"
-	"git.sr.ht/~sircmpwn/aerc/worker/types"
+	//"git.sr.ht/~sircmpwn/aerc/worker/types"
 )
 
 type MessageList struct {
@@ -71,16 +71,16 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
 	uids := store.Uids()
 
 	if ml.conf.Ui.ThreadingEnabled {
-		threads := store.Threads
+		if store.ThreadRoot == nil {
+			return
+		}
 
-		for i := len(threads) - 1; i >= 0; i-- {
-			threads[i].FormatThread(func(thread *types.Thread, threadFmt []rune) bool {
-				if ml.drawRow(ctx, thread.Uid, row, row, &needsHeaders, string(threadFmt)) {
-					return true
-				}
-				row += 1
-				return false
-			})
+		for i := ml.scroll; i < len(store.FlatThreads); i++ {
+			thread := store.FlatThreads[i]
+			if ml.drawRow(ctx, thread.Uid, row, row, &needsHeaders, string(thread.DrawThread())) {
+				break
+			}
+			row += 1
 		}
 	} else {
 		for i := len(uids) - 1 - ml.scroll; i >= 0; i-- {
diff --git a/worker/imap/open.go b/worker/imap/open.go
index bdb794b..1152887 100644
--- a/worker/imap/open.go
+++ b/worker/imap/open.go
@@ -64,18 +64,23 @@ func (imapw *IMAPWorker) handleDirectoryThreaded(
 			Error:   err,
 		}, nil)
 	} else {
-		aercThreads, count := convertThreads(threads)
+		root := &types.Thread{
+			Uid:   0,
+			Dummy: true,
+		}
+		aercThreads, count := convertThreads(threads, root)
+		root.Children = aercThreads
 		imapw.seqMap = make([]uint32, count)
 		imapw.worker.PostMessage(&types.DirectoryThreaded{
-			Message: types.RespondTo(msg),
-			Threads: aercThreads,
+			Message:    types.RespondTo(msg),
+			ThreadRoot: root,
 		}, nil)
 		imapw.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil)
 	}
 }
 
 // This sucks... TODO: find a better way to do this.
-func convertThreads(threads []*sortthread.Thread) ([]*types.Thread, int) {
+func convertThreads(threads []*sortthread.Thread, parent *types.Thread) ([]*types.Thread, int) {
 	if threads == nil {
 		return nil, 0
 	}
@@ -84,11 +89,13 @@ func convertThreads(threads []*sortthread.Thread) ([]*types.Thread, int) {
 
 	for i := 0; i < len(threads); i++ {
 		t := threads[i]
-		children, childCount := convertThreads(t.Children)
 		conv[i] = &types.Thread{
-			Uid:      t.Id,
-			Children: children,
+			Uid:   t.Id,
+			Dummy: false,
 		}
+		conv[i].Parent = parent
+		children, childCount := convertThreads(t.Children, conv[i])
+		conv[i].Children = children
 		count += childCount + 1
 	}
 	return conv, count
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 0b9dc6e..d5f2484 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -159,7 +159,7 @@ type DirectoryContents struct {
 
 type DirectoryThreaded struct {
 	Message
-	Threads []*Thread
+	ThreadRoot *Thread
 }
 
 type SearchResults struct {
diff --git a/worker/types/thread.go b/worker/types/thread.go
index a51a5c4..51cd599 100644
--- a/worker/types/thread.go
+++ b/worker/types/thread.go
@@ -3,44 +3,98 @@ package types
 type Thread struct {
 	Uid      uint32
 	Children []*Thread
+	Parent   *Thread
+	Dummy    bool
 }
 
-func (t *Thread) FormatThread(cb func(*Thread, []rune) bool) {
-	cb(t, []rune{})
-	walkThreads(t.Children, []rune{}, cb)
+func (t *Thread) Find(uid uint32) *Thread {
+	if t.Uid == uid && !t.Dummy {
+		return t
+	}
+
+	for _, child := range t.Children {
+		if needle := child.Find(uid); needle != nil {
+			return needle
+		}
+	}
+
+	return nil
 }
 
-func walkThreads(threads []*Thread, threadFmt []rune,
-	cb func(*Thread, []rune) bool) {
-	if threads == nil {
-		return
-	}
-
-	var (
-		indent          []rune = []rune{'\u0020', '\u0020'}
-		indentConnected []rune = []rune{'\u2502', '\u0020'}
-		arrow           []rune = []rune{'\u251c', '\u2500', '\u003e'}
-		arrowConnected  []rune = []rune{'\u2514', '\u2500', '\u003e'}
-
-		threadPrefix          []rune = append(threadFmt, arrow...)
-		threadPrefixConnected []rune = append(threadFmt, arrowConnected...)
-		nextThread            []rune = append(threadFmt, indent...)
-		nextThreadConnected   []rune = append(threadFmt, indentConnected...)
-	)
-
-	for i := len(threads) - 1; i >= 0; i-- {
-		t := threads[i]
-		if i > 0 && len(threads) > 1 {
-			if cb(t, threadPrefix) {
-				return
-			}
-			walkThreads(t.Children, nextThreadConnected, cb)
+func (t *Thread) Traverse(dummies bool, cb func(*Thread) bool) {
+	if dummies || !t.Dummy {
+		if cb(t) {
+			return
+		}
+	}
+
+	threads := t.Children
+	for i := 0; i < len(threads); i++ {
+		child := threads[i]
+		child.Traverse(dummies, cb)
+	}
+}
+
+var (
+	BINDENT []rune = []rune{'\u0020', '\u0020'}
+	CINDENT []rune = []rune{'\u2502', '\u0020'}
+	LARROW  []rune = []rune{'\u2514', '\u2500', '\u003e'}
+	TARROW  []rune = []rune{'\u252c', '\u2500', '\u003e'}
+	IARROW  []rune = []rune{'\u251c', '\u2500', '\u003e'}
+)
+
+func (t *Thread) isRoot() bool {
+	return t.Dummy && t.Parent == nil
+}
+
+func (t *Thread) isDummyRoot() bool {
+	p := t
+	for p.Dummy {
+		if p.isRoot() {
+			return true
+		}
+		p = p.Parent
+	}
+	return false
+}
+
+func (t *Thread) DrawThread() []rune {
+	var line []rune
+	if t.Parent.isRoot() {
+		return line
+	}
+
+	children := t.Parent.Children
+	if t.Parent.Dummy {
+		children := t.Parent.Children
+		if len(children) > 1 && t == children[0] {
+			line = TARROW
+		} else if len(children) > 1 && t != children[len(children)-1] {
+			line = IARROW
+		} else if len(children) > 1 {
+			line = LARROW
+		}
+	} else {
+		if len(children) > 1 && t != children[len(children)-1] {
+			line = IARROW
 		} else {
-			if cb(t, threadPrefixConnected) {
-				return
+			line = LARROW
+		}
+	}
+
+	p := t.Parent
+
+	for p != nil && !p.Parent.isDummyRoot() {
+		if !p.Dummy {
+			children := p.Parent.Children
+			if len(children) > 1 && p != children[len(children)-1] {
+				line = append(CINDENT, line...)
+			} else {
+				line = append(BINDENT, line...)
 			}
-			walkThreads(t.Children, nextThread, cb)
 		}
+		p = p.Parent
 	}
 
+	return line
 }
-- 
2.23.0

[PATCH v2 8/9] [WIP] Add manual threading to the pipeline Export this patch

Signed-off-by: Kevin Kuehler <keur@xcf.berkeley.edu>
---
 lib/msgstore.go          | 13 +++++++++++--
 widgets/account.go       |  4 ++++
 worker/imap/open.go      | 17 +++++++++++++++++
 worker/imap/worker.go    |  2 ++
 worker/types/messages.go |  9 +++++++++
 5 files changed, 43 insertions(+), 2 deletions(-)

diff --git a/lib/msgstore.go b/lib/msgstore.go
index ccde2c2..bc55255 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -171,6 +171,15 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 	case *types.DirectoryInfo:
 		store.DirInfo = *msg.Info
 		if store.thread {
+			store.worker.PostAction(&types.FetchNativeThreadSupport{}, nil)
+		} else {
+			store.worker.PostAction(&types.FetchDirectoryContents{
+				SortCriteria: store.defaultSortCriteria,
+			}, nil)
+		}
+		update = true
+	case *types.NativeThreadSupport:
+		if msg.HasSupport {
 			store.worker.PostAction(&types.FetchDirectoryThreaded{
 				SortCriteria: store.defaultSortCriteria,
 			}, nil)
@@ -178,8 +187,8 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 			store.worker.PostAction(&types.FetchDirectoryContents{
 				SortCriteria: store.defaultSortCriteria,
 			}, nil)
+			update = true
 		}
-		update = true
 	case *types.DirectoryThreaded:
 		var (
 			uids      []uint32
@@ -190,7 +199,7 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 		msg.ThreadRoot.Traverse(false, func(t *types.Thread) bool {
 			uid := t.Uid
 			uids = append([]uint32{uid}, uids...)
-			flattened = append([]*types.Thread{t}, flattened...)
+			flattened = append(flattened, t)
 			if msg, ok := store.Messages[uid]; ok {
 				newMap[uid] = msg
 			} else {
diff --git a/widgets/account.go b/widgets/account.go
index ebc321d..d3f94cc 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -239,6 +239,10 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
 		if store, ok := acct.dirlist.SelectedMsgStore(); ok {
 			store.Update(msg)
 		}
+	case *types.NativeThreadSupport:
+		if store, ok := acct.dirlist.SelectedMsgStore(); ok {
+			store.Update(msg)
+		}
 	case *types.DirectoryThreaded:
 		if store, ok := acct.dirlist.SelectedMsgStore(); ok {
 			store.Update(msg)
diff --git a/worker/imap/open.go b/worker/imap/open.go
index 1152887..c7a479a 100644
--- a/worker/imap/open.go
+++ b/worker/imap/open.go
@@ -50,6 +50,23 @@ func (imapw *IMAPWorker) handleFetchDirectoryContents(
 	}
 }
 
+func (imapw *IMAPWorker) handleNativeThreadSupport(
+	msg *types.FetchNativeThreadSupport) {
+	hasSupport, err := imapw.client.tc.SupportThread()
+	if err != nil {
+		imapw.worker.PostMessage(&types.Error{
+			Message: types.RespondTo(msg),
+			Error:   err,
+		}, nil)
+	} else {
+		imapw.worker.PostMessage(&types.NativeThreadSupport{
+			Message:    types.RespondTo(msg),
+			HasSupport: hasSupport,
+		}, nil)
+		imapw.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil)
+	}
+}
+
 func (imapw *IMAPWorker) handleDirectoryThreaded(
 	msg *types.FetchDirectoryThreaded) {
 	imapw.worker.Logger.Printf("Fetching threaded UID list")
diff --git a/worker/imap/worker.go b/worker/imap/worker.go
index 1ba3774..ed1ad9e 100644
--- a/worker/imap/worker.go
+++ b/worker/imap/worker.go
@@ -165,6 +165,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
 		w.handleFetchDirectoryContents(msg)
 	case *types.FetchDirectoryThreaded:
 		w.handleDirectoryThreaded(msg)
+	case *types.FetchNativeThreadSupport:
+		w.handleNativeThreadSupport(msg)
 	case *types.CreateDirectory:
 		w.handleCreateDirectory(msg)
 	case *types.FetchMessageHeaders:
diff --git a/worker/types/messages.go b/worker/types/messages.go
index d5f2484..2cde38f 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -81,6 +81,10 @@ type FetchDirectoryContents struct {
 	SortCriteria []*SortCriterion
 }
 
+type FetchNativeThreadSupport struct {
+	Message
+}
+
 type FetchDirectoryThreaded struct {
 	Message
 	SortCriteria []*SortCriterion
@@ -157,6 +161,11 @@ type DirectoryContents struct {
 	Uids []uint32
 }
 
+type NativeThreadSupport struct {
+	Message
+	HasSupport bool
+}
+
 type DirectoryThreaded struct {
 	Message
 	ThreadRoot *Thread
-- 
2.23.0

[PATCH v2 9/9] [WIP] Add SupportThread to notmuch and maildir Export this patch

* notmuch: add placeholder since it has native thread support
* maildir: Return false since it does not have native thread support

Signed-off-by: Kevin Kuehler <keur@xcf.berkeley.edu>
---
 worker/maildir/worker.go | 11 +++++++++++
 worker/notmuch/worker.go | 12 ++++++++++++
 2 files changed, 23 insertions(+)

diff --git a/worker/maildir/worker.go b/worker/maildir/worker.go
index 1df4e09..3ce396a 100644
--- a/worker/maildir/worker.go
+++ b/worker/maildir/worker.go
@@ -170,6 +170,8 @@ func (w *Worker) handleMessage(msg types.WorkerMessage) error {
 		return w.handleOpenDirectory(msg)
 	case *types.FetchDirectoryContents:
 		return w.handleFetchDirectoryContents(msg)
+	case *types.FetchNativeThreadSupport:
+		return w.handleFetchNativeThreadSupport(msg)
 	case *types.CreateDirectory:
 		return w.handleCreateDirectory(msg)
 	case *types.FetchMessageHeaders:
@@ -291,6 +293,15 @@ func (w *Worker) handleFetchDirectoryContents(
 	return nil
 }
 
+func (w *Worker) handleFetchNativeThreadSupport(
+	msg *types.FetchNativeThreadSupport) error {
+	w.worker.PostMessage(&types.NativeThreadSupport{
+		Message:    types.RespondTo(msg),
+		HasSupport: false,
+	}, nil)
+	return nil
+}
+
 func (w *Worker) sort(uids []uint32, criteria []*types.SortCriterion) ([]uint32, error) {
 	if len(criteria) == 0 {
 		return uids, nil
diff --git a/worker/notmuch/worker.go b/worker/notmuch/worker.go
index 96adc29..c0233f5 100644
--- a/worker/notmuch/worker.go
+++ b/worker/notmuch/worker.go
@@ -80,6 +80,8 @@ func (w *worker) handleMessage(msg types.WorkerMessage) error {
 		return w.handleListDirectories(msg)
 	case *types.OpenDirectory:
 		return w.handleOpenDirectory(msg)
+	case *types.FetchNativeThreadSupport:
+		return w.handleFetchNativeThreadSupport(msg)
 	case *types.FetchDirectoryContents:
 		return w.handleFetchDirectoryContents(msg)
 	case *types.FetchMessageHeaders:
@@ -194,6 +196,16 @@ func (w *worker) handleFetchDirectoryContents(
 	return nil
 }
 
+func (w *worker) handleFetchNativeThreadSupport(
+	msg *types.FetchNativeThreadSupport) error {
+	// FIXME
+	w.w.PostMessage(&types.NativeThreadSupport{
+		Message:    types.RespondTo(msg),
+		HasSupport: false,
+	}, nil)
+	return nil
+}
+
 func (w *worker) handleFetchMessageHeaders(
 	msg *types.FetchMessageHeaders) error {
 	for _, uid := range msg.Uids {
-- 
2.23.0
View this thread in the archives