~sircmpwn/aerc

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
10 3

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

Details
Message ID
<20191028190708.253770-1-keur@xcf.berkeley.edu>
DKIM signature
missing
Download raw message
Patch: +43 -3
* 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

Details
Message ID
<20191028190708.253770-2-keur@xcf.berkeley.edu>
In-Reply-To
<20191028190708.253770-1-keur@xcf.berkeley.edu> (view parent)
DKIM signature
missing
Download raw message
Patch: +56 -2
* 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

Details
Message ID
<20191028190708.253770-3-keur@xcf.berkeley.edu>
In-Reply-To
<20191028190708.253770-1-keur@xcf.berkeley.edu> (view parent)
DKIM signature
missing
Download raw message
Patch: +54 -44
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

Details
Message ID
<20191028190708.253770-4-keur@xcf.berkeley.edu>
In-Reply-To
<20191028190708.253770-1-keur@xcf.berkeley.edu> (view parent)
DKIM signature
missing
Download raw message
Patch: +40 -0
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

Details
Message ID
<20191028190708.253770-5-keur@xcf.berkeley.edu>
In-Reply-To
<20191028190708.253770-1-keur@xcf.berkeley.edu> (view parent)
DKIM signature
missing
Download raw message
Patch: +23 -1
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()

Details
Message ID
<20191028190708.253770-6-keur@xcf.berkeley.edu>
In-Reply-To
<20191028190708.253770-1-keur@xcf.berkeley.edu> (view parent)
DKIM signature
missing
Download raw message
Patch: +27 -12
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

Details
Message ID
<20191028190708.253770-7-keur@xcf.berkeley.edu>
In-Reply-To
<20191028190708.253770-1-keur@xcf.berkeley.edu> (view parent)
DKIM signature
missing
Download raw message
Patch: +278 -77
* 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

Details
Message ID
<20191028190708.253770-8-keur@xcf.berkeley.edu>
In-Reply-To
<20191028190708.253770-1-keur@xcf.berkeley.edu> (view parent)
DKIM signature
missing
Download raw message
Patch: +43 -2
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

Details
Message ID
<20191028190708.253770-9-keur@xcf.berkeley.edu>
In-Reply-To
<20191028190708.253770-1-keur@xcf.berkeley.edu> (view parent)
DKIM signature
missing
Download raw message
Patch: +23 -0
* 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
Details
Message ID
<20200217141109.dmge6xgikhu6zzwa@chew.redmars.org>
In-Reply-To
<20191028190708.253770-1-keur@xcf.berkeley.edu> (view parent)
DKIM signature
missing
Download raw message
I would love to try this patch set out. Can you let me know a ref from
the upstream master branch upon which this applies cleanly? I've tried
guessing a few from around ~October 2019 but "git am" has failed for 
each guess.

TIA!

(This is one advantage that the PR workflow model has, btw, over mailing 
patch sets: you know what state the upstream branch was when the patch 
was authored).
Details
Message ID
<C0OI0GRT78U6.VVHGN0J6T1OB@homura>
In-Reply-To
<20200217141109.dmge6xgikhu6zzwa@chew.redmars.org> (view parent)
DKIM signature
missing
Download raw message
On Mon Feb 17, 2020 at 2:11 PM, Jonathan Dowland wrote:
> (This is one advantage that the PR workflow model has, btw, over mailing
> patch sets: you know what state the upstream branch was when the patch
> was authored).

Original email was sent on 2019-10-28 19:07:16, therefore:

git checkout master@'{2019-10-28 19:07:16}'

Is probably sufficient.
Review patch Export thread (mbox)