aboutsummaryrefslogblamecommitdiffstats
path: root/lib/threadbuilder.go
blob: c87d0bfb1fbb404567d10409c942fe143f7f8920 (plain) (tree)

















































































































































































































































                                                                                                  
package lib

import (
	"log"
	"time"

	"git.sr.ht/~rjarry/aerc/models"
	"git.sr.ht/~rjarry/aerc/worker/types"
	"github.com/gatherstars-com/jwz"
)

type UidStorer interface {
	Uids() []uint32
}

type ThreadBuilder struct {
	threadBlocks   map[uint32]jwz.Threadable
	messageidToUid map[string]uint32
	seen           map[uint32]bool
	store          UidStorer
	logger         *log.Logger
}

func NewThreadBuilder(store UidStorer, logger *log.Logger) *ThreadBuilder {
	tb := &ThreadBuilder{
		threadBlocks:   make(map[uint32]jwz.Threadable),
		messageidToUid: make(map[string]uint32),
		seen:           make(map[uint32]bool),
		store:          store,
		logger:         logger,
	}
	return tb
}

func (builder *ThreadBuilder) Update(msg *models.MessageInfo) {
	if msg != nil {
		if threadable := newThreadable(msg); threadable != nil {
			builder.messageidToUid[threadable.MessageThreadID()] = msg.Uid
			builder.threadBlocks[msg.Uid] = threadable
		}
	}
}

func (builder *ThreadBuilder) Threads() []*types.Thread {
	start := time.Now()

	threads := builder.buildAercThreads(builder.generateStructure())

	elapsed := time.Since(start)
	builder.logger.Println("ThreadBuilder:", len(threads), "threads created in", elapsed)

	return threads
}

func (builder *ThreadBuilder) generateStructure() jwz.Threadable {
	jwzThreads := make([]jwz.Threadable, 0, len(builder.threadBlocks))
	for _, uid := range builder.store.Uids() {
		if thr, ok := builder.threadBlocks[uid]; ok {
			jwzThreads = append(jwzThreads, thr)
		}
	}

	threader := jwz.NewThreader()
	threadStructure, err := threader.ThreadSlice(jwzThreads)
	if err != nil {
		builder.logger.Printf("ThreadBuilder: threading operation return error: %#v", err)
	}
	return threadStructure
}

func (builder *ThreadBuilder) buildAercThreads(structure jwz.Threadable) []*types.Thread {
	threads := make([]*types.Thread, 0, len(builder.threadBlocks))
	if structure == nil {
		for _, uid := range builder.store.Uids() {
			threads = append(threads, &types.Thread{Uid: uid})
		}
	} else {
		// fill threads with nil messages
		for _, uid := range builder.store.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)
		}
	}
}

// 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
	}
	refs, err := t.MsgInfo.References()
	if err != nil || len(refs) == 0 {
		inreplyto, err := t.MsgInfo.InReplyTo()
		if err != nil {
			return nil
		}
		refs = []string{inreplyto}
	}
	return refs
}

func (t *threadable) Subject() string {
	// deactivate threading by subject for now
	return ""

	if t.IsDummy() || t.MsgInfo == nil || t.MsgInfo.Envelope == nil {
		return ""
	}
	return t.MsgInfo.Envelope.Subject
}

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
}