aboutsummaryrefslogtreecommitdiffstats
path: root/lib/threadbuilder.go
diff options
context:
space:
mode:
Diffstat (limited to 'lib/threadbuilder.go')
-rw-r--r--lib/threadbuilder.go242
1 files changed, 242 insertions, 0 deletions
diff --git a/lib/threadbuilder.go b/lib/threadbuilder.go
new file mode 100644
index 00000000..c87d0bfb
--- /dev/null
+++ b/lib/threadbuilder.go
@@ -0,0 +1,242 @@
+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
+}