5ca6022d00
Signed-off-by: Moritz Poldrack <moritz@poldrack.dev> Acked-by: Robin Jarry <robin@jarry.cc>
301 lines
7.3 KiB
Go
301 lines
7.3 KiB
Go
package lib
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"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
|
|
}
|
|
|
|
func NewThreadBuilder() *ThreadBuilder {
|
|
tb := &ThreadBuilder{
|
|
threadBlocks: make(map[uint32]jwz.Threadable),
|
|
messageidToUid: make(map[string]uint32),
|
|
seen: make(map[uint32]bool),
|
|
}
|
|
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))
|
|
for i := len(threads) - 1; i >= 0; i-- {
|
|
_ = threads[i].Walk(func(t *types.Thread, level int, currentErr error) error {
|
|
uids = append(uids, t.Uid)
|
|
return nil
|
|
})
|
|
}
|
|
// copy in reverse as msgList displays backwards
|
|
for i, j := 0, len(uids)-1; i < j; i, j = i+1, j-1 {
|
|
uids[i], uids[j] = uids[j], uids[i]
|
|
}
|
|
builder.threadedUids = uids
|
|
}
|
|
|
|
// 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
|
|
}
|