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
}