aerc/lib/threadbuilder.go
Koni Marti c5face0b6f store: reverse message list order with iterators
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>
2022-10-27 22:44:39 +02:00

308 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
}