c5face0b6f
Reverse the order of the messages in the message list. The complexity of reversing the order is abstracted away by the iterators. To reverse the message list, add the following to your aerc.conf: [ui] reverse-msglist-order=true Thanks to |cos| for sharing his initial implementation of reversing the order in the message list [0]. [0]: https://git.netizen.se/aerc/commit/?h=topic/asc_sort_imap Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
307 lines
7.5 KiB
Go
307 lines
7.5 KiB
Go
package lib
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"git.sr.ht/~rjarry/aerc/lib/iterator"
|
|
"git.sr.ht/~rjarry/aerc/logging"
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
|
"github.com/gatherstars-com/jwz"
|
|
)
|
|
|
|
type ThreadBuilder struct {
|
|
sync.Mutex
|
|
threadBlocks map[uint32]jwz.Threadable
|
|
messageidToUid map[string]uint32
|
|
seen map[uint32]bool
|
|
threadedUids []uint32
|
|
iterFactory iterator.Factory
|
|
}
|
|
|
|
func NewThreadBuilder(i iterator.Factory) *ThreadBuilder {
|
|
tb := &ThreadBuilder{
|
|
threadBlocks: make(map[uint32]jwz.Threadable),
|
|
messageidToUid: make(map[string]uint32),
|
|
seen: make(map[uint32]bool),
|
|
iterFactory: i,
|
|
}
|
|
return tb
|
|
}
|
|
|
|
// Uids returns the uids in threading order
|
|
func (builder *ThreadBuilder) Uids() []uint32 {
|
|
builder.Lock()
|
|
defer builder.Unlock()
|
|
|
|
if builder.threadedUids == nil {
|
|
return []uint32{}
|
|
}
|
|
return builder.threadedUids
|
|
}
|
|
|
|
// Update updates the thread builder with a new message header
|
|
func (builder *ThreadBuilder) Update(msg *models.MessageInfo) {
|
|
builder.Lock()
|
|
defer builder.Unlock()
|
|
|
|
if msg != nil {
|
|
if threadable := newThreadable(msg); threadable != nil {
|
|
builder.messageidToUid[threadable.MessageThreadID()] = msg.Uid
|
|
builder.threadBlocks[msg.Uid] = threadable
|
|
}
|
|
}
|
|
}
|
|
|
|
// Threads returns a slice of threads for the given list of uids
|
|
func (builder *ThreadBuilder) Threads(uids []uint32) []*types.Thread {
|
|
builder.Lock()
|
|
defer builder.Unlock()
|
|
|
|
start := time.Now()
|
|
|
|
threads := builder.buildAercThreads(builder.generateStructure(uids), uids)
|
|
|
|
// sort threads according to uid ordering
|
|
builder.sortThreads(threads, uids)
|
|
|
|
// rebuild uids from threads
|
|
builder.RebuildUids(threads)
|
|
|
|
elapsed := time.Since(start)
|
|
logging.Infof("%d threads created in %s", len(threads), elapsed)
|
|
|
|
return threads
|
|
}
|
|
|
|
func (builder *ThreadBuilder) generateStructure(uids []uint32) jwz.Threadable {
|
|
jwzThreads := make([]jwz.Threadable, 0, len(builder.threadBlocks))
|
|
for _, uid := range uids {
|
|
if thr, ok := builder.threadBlocks[uid]; ok {
|
|
jwzThreads = append(jwzThreads, thr)
|
|
}
|
|
}
|
|
|
|
threader := jwz.NewThreader()
|
|
threadStructure, err := threader.ThreadSlice(jwzThreads)
|
|
if err != nil {
|
|
logging.Errorf("failed slicing threads: %v", err)
|
|
}
|
|
return threadStructure
|
|
}
|
|
|
|
func (builder *ThreadBuilder) buildAercThreads(structure jwz.Threadable, uids []uint32) []*types.Thread {
|
|
threads := make([]*types.Thread, 0, len(builder.threadBlocks))
|
|
if structure == nil {
|
|
for _, uid := range uids {
|
|
threads = append(threads, &types.Thread{Uid: uid})
|
|
}
|
|
} else {
|
|
// fill threads with nil messages
|
|
for _, uid := range uids {
|
|
if _, ok := builder.threadBlocks[uid]; !ok {
|
|
threads = append(threads, &types.Thread{Uid: uid})
|
|
}
|
|
}
|
|
// append the on-the-fly created aerc threads
|
|
root := &types.Thread{Uid: 0}
|
|
builder.seen = make(map[uint32]bool)
|
|
builder.buildTree(structure, root)
|
|
for iter := root.FirstChild; iter != nil; iter = iter.NextSibling {
|
|
iter.Parent = nil
|
|
threads = append(threads, iter)
|
|
}
|
|
}
|
|
return threads
|
|
}
|
|
|
|
// buildTree recursively translates the jwz threads structure into aerc threads
|
|
// builder.seen is used to avoid potential double-counting and should be empty
|
|
// on first call of this function
|
|
func (builder *ThreadBuilder) buildTree(treeNode jwz.Threadable, target *types.Thread) {
|
|
if treeNode == nil {
|
|
return
|
|
}
|
|
|
|
// deal with child
|
|
uid, ok := builder.messageidToUid[treeNode.MessageThreadID()]
|
|
if _, seen := builder.seen[uid]; ok && !seen {
|
|
builder.seen[uid] = true
|
|
childNode := &types.Thread{Uid: uid, Parent: target}
|
|
target.OrderedInsert(childNode)
|
|
builder.buildTree(treeNode.GetChild(), childNode)
|
|
} else {
|
|
builder.buildTree(treeNode.GetChild(), target)
|
|
}
|
|
|
|
// deal with siblings
|
|
for next := treeNode.GetNext(); next != nil; next = next.GetNext() {
|
|
|
|
uid, ok := builder.messageidToUid[next.MessageThreadID()]
|
|
if _, seen := builder.seen[uid]; ok && !seen {
|
|
builder.seen[uid] = true
|
|
nn := &types.Thread{Uid: uid, Parent: target}
|
|
target.OrderedInsert(nn)
|
|
builder.buildTree(next.GetChild(), nn)
|
|
} else {
|
|
builder.buildTree(next.GetChild(), target)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (builder *ThreadBuilder) sortThreads(threads []*types.Thread, orderedUids []uint32) {
|
|
types.SortThreadsBy(threads, orderedUids)
|
|
}
|
|
|
|
// RebuildUids rebuilds the uids from the given slice of threads
|
|
func (builder *ThreadBuilder) RebuildUids(threads []*types.Thread) {
|
|
uids := make([]uint32, 0, len(threads))
|
|
iterT := builder.iterFactory.NewIterator(threads)
|
|
for iterT.Next() {
|
|
_ = iterT.Value().(*types.Thread).Walk(
|
|
func(t *types.Thread, level int, currentErr error) error {
|
|
uids = append(uids, t.Uid)
|
|
return nil
|
|
})
|
|
}
|
|
result := make([]uint32, 0, len(uids))
|
|
iterU := builder.iterFactory.NewIterator(uids)
|
|
for iterU.Next() {
|
|
result = append(result, iterU.Value().(uint32))
|
|
}
|
|
builder.threadedUids = result
|
|
}
|
|
|
|
// threadable implements the jwz.threadable interface which is required for the
|
|
// jwz threading algorithm
|
|
type threadable struct {
|
|
MsgInfo *models.MessageInfo
|
|
MessageId string
|
|
Next jwz.Threadable
|
|
Parent jwz.Threadable
|
|
Child jwz.Threadable
|
|
Dummy bool
|
|
}
|
|
|
|
func newThreadable(msg *models.MessageInfo) *threadable {
|
|
msgid, err := msg.MsgId()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &threadable{
|
|
MessageId: msgid,
|
|
MsgInfo: msg,
|
|
Next: nil,
|
|
Parent: nil,
|
|
Child: nil,
|
|
Dummy: false,
|
|
}
|
|
}
|
|
|
|
func (t *threadable) MessageThreadID() string {
|
|
return t.MessageId
|
|
}
|
|
|
|
func (t *threadable) MessageThreadReferences() []string {
|
|
if t.IsDummy() || t.MsgInfo == nil {
|
|
return nil
|
|
}
|
|
irp, err := t.MsgInfo.InReplyTo()
|
|
if err != nil {
|
|
irp = ""
|
|
}
|
|
refs, err := t.MsgInfo.References()
|
|
if err != nil || len(refs) == 0 {
|
|
if irp == "" {
|
|
return nil
|
|
}
|
|
refs = []string{irp}
|
|
}
|
|
return cleanRefs(t.MessageThreadID(), irp, refs)
|
|
}
|
|
|
|
// cleanRefs cleans up the references headers for threading
|
|
// 1) message-id should not be part of the references
|
|
// 2) no message-id should occur twice (avoid circularities)
|
|
// 3) in-reply-to header should not be at the beginning
|
|
func cleanRefs(m, irp string, refs []string) []string {
|
|
considered := make(map[string]interface{})
|
|
cleanRefs := make([]string, 0, len(refs))
|
|
for _, r := range refs {
|
|
if _, seen := considered[r]; r != m && !seen {
|
|
considered[r] = nil
|
|
cleanRefs = append(cleanRefs, r)
|
|
}
|
|
}
|
|
if irp != "" && len(cleanRefs) > 0 {
|
|
if cleanRefs[0] == irp {
|
|
cleanRefs = append(cleanRefs[1:], irp)
|
|
}
|
|
}
|
|
return cleanRefs
|
|
}
|
|
|
|
func (t *threadable) Subject() string {
|
|
// deactivate threading by subject for now
|
|
return ""
|
|
}
|
|
|
|
func (t *threadable) SimplifiedSubject() string {
|
|
return ""
|
|
}
|
|
|
|
func (t *threadable) SubjectIsReply() bool {
|
|
return false
|
|
}
|
|
|
|
func (t *threadable) SetNext(next jwz.Threadable) {
|
|
t.Next = next
|
|
}
|
|
|
|
func (t *threadable) SetChild(kid jwz.Threadable) {
|
|
t.Child = kid
|
|
if kid != nil {
|
|
kid.SetParent(t)
|
|
}
|
|
}
|
|
|
|
func (t *threadable) SetParent(parent jwz.Threadable) {
|
|
t.Parent = parent
|
|
}
|
|
|
|
func (t *threadable) GetNext() jwz.Threadable {
|
|
return t.Next
|
|
}
|
|
|
|
func (t *threadable) GetChild() jwz.Threadable {
|
|
return t.Child
|
|
}
|
|
|
|
func (t *threadable) GetParent() jwz.Threadable {
|
|
return t.Parent
|
|
}
|
|
|
|
func (t *threadable) GetDate() time.Time {
|
|
if t.IsDummy() {
|
|
if t.GetChild() != nil {
|
|
return t.GetChild().GetDate()
|
|
}
|
|
return time.Unix(0, 0)
|
|
}
|
|
if t.MsgInfo == nil || t.MsgInfo.Envelope == nil {
|
|
return time.Unix(0, 0)
|
|
}
|
|
return t.MsgInfo.Envelope.Date
|
|
}
|
|
|
|
func (t *threadable) MakeDummy(forID string) jwz.Threadable {
|
|
return &threadable{
|
|
MessageId: forID,
|
|
Dummy: true,
|
|
}
|
|
}
|
|
|
|
func (t *threadable) IsDummy() bool {
|
|
return t.Dummy
|
|
}
|