65ae87a524
Apply the user-defined sort criteria to the message with the highest
uid in a threaded discussion. Restore the default sort order when
leaving threading mode.
Commit 7811620eb8
("threading: implement on-the-fly message
threading") introduced message threading with the threaded messages
being only sorted by their message uids irrespective of the defined sorting
criteria. It did not restore the default sort order either.
Reported-by: Sebastien Binet <s@sbinet.org>
Signed-off-by: Koni Marti <koni.marti@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
273 lines
6.7 KiB
Go
273 lines
6.7 KiB
Go
package lib
|
|
|
|
import (
|
|
"log"
|
|
"time"
|
|
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
|
"github.com/gatherstars-com/jwz"
|
|
)
|
|
|
|
type ThreadBuilder struct {
|
|
threadBlocks map[uint32]jwz.Threadable
|
|
messageidToUid map[string]uint32
|
|
seen map[uint32]bool
|
|
threadedUids []uint32
|
|
logger *log.Logger
|
|
}
|
|
|
|
func NewThreadBuilder(logger *log.Logger) *ThreadBuilder {
|
|
tb := &ThreadBuilder{
|
|
threadBlocks: make(map[uint32]jwz.Threadable),
|
|
messageidToUid: make(map[string]uint32),
|
|
seen: make(map[uint32]bool),
|
|
logger: logger,
|
|
}
|
|
return tb
|
|
}
|
|
|
|
// Uids returns the uids in threading order
|
|
func (builder *ThreadBuilder) Uids() []uint32 {
|
|
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) {
|
|
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 {
|
|
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)
|
|
builder.logger.Println("ThreadBuilder:", len(threads), "threads created in", 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 {
|
|
builder.logger.Printf("ThreadBuilder: threading operation return error: %#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
|
|
}
|
|
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
|
|
}
|