aerc/lib/msgstore.go
Koni Marti e50ab59284 sort: keep sort criteria applied to folder
Keep the sort criteria applied to the selected folder until the default
sort order should be restored. Call the sort command without arguments
to restore the default sort order.

The current behavior is that the default sort order is restored as soon
as the folder reloads. This happens often and then the results of the
sort command are lost. This makes the sort command not very
user-friendly. Instead, we should keep the sort criteria applied until
the user explicitly wants to restore the default sort order again.

Signed-off-by: Koni Marti <koni.marti@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2022-03-25 13:07:20 +01:00

752 lines
17 KiB
Go

package lib
import (
"io"
"time"
"git.sr.ht/~rjarry/aerc/lib/sort"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
)
// Accesses to fields must be guarded by MessageStore.Lock/Unlock
type MessageStore struct {
Deleted map[uint32]interface{}
DirInfo models.DirectoryInfo
Messages map[uint32]*models.MessageInfo
Sorting bool
// Ordered list of known UIDs
uids []uint32
Threads []*types.Thread
selected int
bodyCallbacks map[uint32][]func(*types.FullMessage)
headerCallbacks map[uint32][]func(*types.MessageInfo)
//marking
marked map[uint32]struct{}
visualStartUid uint32
visualMarkMode bool
// Search/filter results
results []uint32
resultIndex int
filtered []uint32
filter bool
sortCriteria []*types.SortCriterion
thread bool
buildThreads bool
builder *ThreadBuilder
// Map of uids we've asked the worker to fetch
onUpdate func(store *MessageStore) // TODO: multiple onUpdate handlers
onFilterChange func(store *MessageStore)
onUpdateDirs func()
pendingBodies map[uint32]interface{}
pendingHeaders map[uint32]interface{}
worker *types.Worker
triggerNewEmail func(*models.MessageInfo)
triggerDirectoryChange func()
dirInfoUpdateDebounce *time.Timer
dirInfoUpdateDelay time.Duration
}
func NewMessageStore(worker *types.Worker,
dirInfo *models.DirectoryInfo,
defaultSortCriteria []*types.SortCriterion,
thread bool,
triggerNewEmail func(*models.MessageInfo),
triggerDirectoryChange func()) *MessageStore {
dirInfoUpdateDelay := 5 * time.Second
return &MessageStore{
Deleted: make(map[uint32]interface{}),
DirInfo: *dirInfo,
Messages: make(map[uint32]*models.MessageInfo),
selected: 0,
marked: make(map[uint32]struct{}),
bodyCallbacks: make(map[uint32][]func(*types.FullMessage)),
headerCallbacks: make(map[uint32][]func(*types.MessageInfo)),
thread: thread,
sortCriteria: defaultSortCriteria,
pendingBodies: make(map[uint32]interface{}),
pendingHeaders: make(map[uint32]interface{}),
worker: worker,
triggerNewEmail: triggerNewEmail,
triggerDirectoryChange: triggerDirectoryChange,
dirInfoUpdateDelay: dirInfoUpdateDelay,
dirInfoUpdateDebounce: time.NewTimer(dirInfoUpdateDelay),
}
}
func (store *MessageStore) FetchHeaders(uids []uint32,
cb func(*types.MessageInfo)) {
// TODO: this could be optimized by pre-allocating toFetch and trimming it
// at the end. In practice we expect to get most messages back in one frame.
var toFetch []uint32
for _, uid := range uids {
if _, ok := store.pendingHeaders[uid]; !ok {
toFetch = append(toFetch, uid)
store.pendingHeaders[uid] = nil
if cb != nil {
if list, ok := store.headerCallbacks[uid]; ok {
store.headerCallbacks[uid] = append(list, cb)
} else {
store.headerCallbacks[uid] = []func(*types.MessageInfo){cb}
}
}
}
}
if len(toFetch) > 0 {
store.worker.PostAction(&types.FetchMessageHeaders{Uids: toFetch}, nil)
}
}
func (store *MessageStore) FetchFull(uids []uint32, cb func(*types.FullMessage)) {
// TODO: this could be optimized by pre-allocating toFetch and trimming it
// at the end. In practice we expect to get most messages back in one frame.
var toFetch []uint32
for _, uid := range uids {
if _, ok := store.pendingBodies[uid]; !ok {
toFetch = append(toFetch, uid)
store.pendingBodies[uid] = nil
if cb != nil {
if list, ok := store.bodyCallbacks[uid]; ok {
store.bodyCallbacks[uid] = append(list, cb)
} else {
store.bodyCallbacks[uid] = []func(*types.FullMessage){cb}
}
}
}
}
if len(toFetch) > 0 {
store.worker.PostAction(&types.FetchFullMessages{
Uids: toFetch,
}, func(msg types.WorkerMessage) {
switch msg.(type) {
case *types.Error:
for _, uid := range toFetch {
delete(store.bodyCallbacks, uid)
}
}
})
}
}
func (store *MessageStore) FetchBodyPart(uid uint32, part []int, cb func(io.Reader)) {
store.worker.PostAction(&types.FetchMessageBodyPart{
Uid: uid,
Part: part,
}, func(resp types.WorkerMessage) {
msg, ok := resp.(*types.MessageBodyPart)
if !ok {
return
}
cb(msg.Part.Reader)
})
}
func merge(to *models.MessageInfo, from *models.MessageInfo) {
if from.BodyStructure != nil {
to.BodyStructure = from.BodyStructure
}
if from.Envelope != nil {
to.Envelope = from.Envelope
}
to.Flags = from.Flags
to.Labels = from.Labels
if from.Size != 0 {
to.Size = from.Size
}
var zero time.Time
if from.InternalDate != zero {
to.InternalDate = from.InternalDate
}
}
func (store *MessageStore) Update(msg types.WorkerMessage) {
update := false
directoryChange := false
switch msg := msg.(type) {
case *types.DirectoryInfo:
store.DirInfo = *msg.Info
store.Sort(store.sortCriteria, nil)
update = true
case *types.DirectoryContents:
newMap := make(map[uint32]*models.MessageInfo)
for _, uid := range msg.Uids {
if msg, ok := store.Messages[uid]; ok {
newMap[uid] = msg
} else {
newMap[uid] = nil
directoryChange = true
}
}
store.Messages = newMap
store.uids = msg.Uids
update = true
case *types.DirectoryThreaded:
var uids []uint32
newMap := make(map[uint32]*models.MessageInfo)
for i := len(msg.Threads) - 1; i >= 0; i-- {
msg.Threads[i].Walk(func(t *types.Thread, level int, currentErr error) error {
uid := t.Uid
uids = append([]uint32{uid}, uids...)
if msg, ok := store.Messages[uid]; ok {
newMap[uid] = msg
} else {
newMap[uid] = nil
directoryChange = true
}
return nil
})
}
store.Messages = newMap
store.uids = uids
store.Threads = msg.Threads
update = true
case *types.MessageInfo:
if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil {
merge(existing, msg.Info)
} else {
store.Messages[msg.Info.Uid] = msg.Info
}
seen := false
recent := false
for _, flag := range msg.Info.Flags {
if flag == models.RecentFlag {
recent = true
} else if flag == models.SeenFlag {
seen = true
}
}
if !seen && recent {
store.triggerNewEmail(msg.Info)
}
if _, ok := store.pendingHeaders[msg.Info.Uid]; msg.Info.Envelope != nil && ok {
delete(store.pendingHeaders, msg.Info.Uid)
if cbs, ok := store.headerCallbacks[msg.Info.Uid]; ok {
for _, cb := range cbs {
cb(msg)
}
}
}
if store.builder != nil {
store.builder.Update(msg.Info)
}
update = true
case *types.FullMessage:
if _, ok := store.pendingBodies[msg.Content.Uid]; ok {
delete(store.pendingBodies, msg.Content.Uid)
if cbs, ok := store.bodyCallbacks[msg.Content.Uid]; ok {
for _, cb := range cbs {
cb(msg)
}
delete(store.bodyCallbacks, msg.Content.Uid)
}
}
case *types.MessagesDeleted:
if len(store.uids) < len(msg.Uids) {
update = true
break
}
toDelete := make(map[uint32]interface{})
for _, uid := range msg.Uids {
toDelete[uid] = nil
delete(store.Messages, uid)
delete(store.Deleted, uid)
delete(store.marked, uid)
}
uids := make([]uint32, len(store.uids)-len(msg.Uids))
j := 0
for _, uid := range store.uids {
if _, deleted := toDelete[uid]; !deleted && j < len(uids) {
uids[j] = uid
j += 1
}
}
store.uids = uids
var newResults []uint32
for _, res := range store.results {
if _, deleted := toDelete[res]; !deleted {
newResults = append(newResults, res)
}
}
store.results = newResults
var newFiltered []uint32
for _, res := range store.filtered {
if _, deleted := toDelete[res]; !deleted {
newFiltered = append(newFiltered, res)
}
}
store.filtered = newFiltered
for _, thread := range store.Threads {
thread.Walk(func(t *types.Thread, _ int, _ error) error {
if _, deleted := toDelete[t.Uid]; deleted {
t.Deleted = true
}
return nil
})
}
update = true
}
if update {
store.update()
}
if directoryChange && store.triggerDirectoryChange != nil {
store.triggerDirectoryChange()
}
}
func (store *MessageStore) OnUpdate(fn func(store *MessageStore)) {
store.onUpdate = fn
}
func (store *MessageStore) OnFilterChange(fn func(store *MessageStore)) {
store.onFilterChange = fn
}
func (store *MessageStore) OnUpdateDirs(fn func()) {
store.onUpdateDirs = fn
}
func (store *MessageStore) update() {
if store.onUpdate != nil {
store.onUpdate(store)
}
if store.onUpdateDirs != nil {
store.onUpdateDirs()
}
if store.BuildThreads() {
store.runThreadBuilder()
}
}
func (store *MessageStore) SetBuildThreads(buildThreads bool) {
// if worker provides threading, don't build our own threads
if store.thread {
return
}
store.buildThreads = buildThreads
if store.BuildThreads() {
store.runThreadBuilder()
}
}
func (store *MessageStore) BuildThreads() bool {
// if worker provides threading, don't build our own threads
if store.thread {
return false
}
return store.buildThreads
}
func (store *MessageStore) runThreadBuilder() {
if store.builder == nil {
store.builder = NewThreadBuilder(store.worker.Logger)
for _, msg := range store.Messages {
store.builder.Update(msg)
}
}
var uids []uint32
if store.filter {
uids = store.filtered
} else {
uids = store.uids
}
store.Threads = store.builder.Threads(uids)
}
func (store *MessageStore) Delete(uids []uint32,
cb func(msg types.WorkerMessage)) {
for _, uid := range uids {
store.Deleted[uid] = nil
}
store.worker.PostAction(&types.DeleteMessages{Uids: uids}, cb)
store.update()
}
func (store *MessageStore) Copy(uids []uint32, dest string, createDest bool,
cb func(msg types.WorkerMessage)) {
if createDest {
store.worker.PostAction(&types.CreateDirectory{
Directory: dest,
Quiet: true,
}, cb)
}
store.worker.PostAction(&types.CopyMessages{
Destination: dest,
Uids: uids,
}, cb)
}
func (store *MessageStore) Move(uids []uint32, dest string, createDest bool,
cb func(msg types.WorkerMessage)) {
for _, uid := range uids {
store.Deleted[uid] = nil
}
if createDest {
store.worker.PostAction(&types.CreateDirectory{
Directory: dest,
Quiet: true,
}, nil) // quiet doesn't return an error, don't want the done cb here
}
store.worker.PostAction(&types.CopyMessages{
Destination: dest,
Uids: uids,
}, func(msg types.WorkerMessage) {
switch msg.(type) {
case *types.Error:
cb(msg)
case *types.Done:
store.worker.PostAction(&types.DeleteMessages{Uids: uids}, cb)
}
})
store.update()
}
func (store *MessageStore) Flag(uids []uint32, flag models.Flag,
enable bool, cb func(msg types.WorkerMessage)) {
store.worker.PostAction(&types.FlagMessages{
Enable: enable,
Flag: flag,
Uids: uids,
}, cb)
}
func (store *MessageStore) Answered(uids []uint32, answered bool,
cb func(msg types.WorkerMessage)) {
store.worker.PostAction(&types.AnsweredMessages{
Answered: answered,
Uids: uids,
}, cb)
}
func (store *MessageStore) Uids() []uint32 {
if store.BuildThreads() && store.builder != nil {
if uids := store.builder.Uids(); len(uids) > 0 {
return uids
}
}
if store.filter {
return store.filtered
}
return store.uids
}
func (store *MessageStore) Selected() *models.MessageInfo {
return store.Messages[store.Uids()[len(store.Uids())-store.selected-1]]
}
func (store *MessageStore) SelectedIndex() int {
return store.selected
}
func (store *MessageStore) Select(index int) {
uids := store.Uids()
store.selected = index
if store.selected < 0 {
store.selected = len(uids) - 1
} else if store.selected > len(uids) {
store.selected = len(uids)
}
store.updateVisual()
}
// Mark sets the marked state on a MessageInfo
func (store *MessageStore) Mark(uid uint32) {
if store.visualMarkMode {
// visual mode has override, bogus input from user
return
}
store.marked[uid] = struct{}{}
}
// Unmark removes the marked state on a MessageInfo
func (store *MessageStore) Unmark(uid uint32) {
if store.visualMarkMode {
// user probably wanted to clear the visual marking
store.ClearVisualMark()
return
}
delete(store.marked, uid)
}
// ToggleMark toggles the marked state on a MessageInfo
func (store *MessageStore) ToggleMark(uid uint32) {
if store.visualMarkMode {
// visual mode has override, bogus input from user
return
}
if store.IsMarked(uid) {
store.Unmark(uid)
} else {
store.Mark(uid)
}
}
// resetMark removes the marking from all messages
func (store *MessageStore) resetMark() {
store.marked = make(map[uint32]struct{})
}
//IsMarked checks whether a MessageInfo has been marked
func (store *MessageStore) IsMarked(uid uint32) bool {
_, marked := store.marked[uid]
return marked
}
//ToggleVisualMark enters or leaves the visual marking mode
func (store *MessageStore) ToggleVisualMark() {
store.visualMarkMode = !store.visualMarkMode
switch store.visualMarkMode {
case true:
// just entered visual mode, reset whatever marking was already done
store.resetMark()
store.visualStartUid = store.Selected().Uid
store.marked[store.visualStartUid] = struct{}{}
case false:
// visual mode ended, nothing to do
return
}
}
//ClearVisualMark leaves the visual marking mode and resets any marking
func (store *MessageStore) ClearVisualMark() {
store.resetMark()
store.visualMarkMode = false
store.visualStartUid = 0
}
// Marked returns the uids of all marked messages
func (store *MessageStore) Marked() []uint32 {
marked := make([]uint32, len(store.marked))
i := 0
for uid := range store.marked {
marked[i] = uid
i++
}
return marked
}
func (store *MessageStore) updateVisual() {
if !store.visualMarkMode {
// nothing to do
return
}
startIdx := store.visualStartIdx()
if startIdx < 0 {
// something deleted the startuid, abort the marking process
store.ClearVisualMark()
return
}
uidLen := len(store.Uids())
// store.selected is the inverted form of the actual array
selectedIdx := uidLen - store.selected - 1
var visUids []uint32
if selectedIdx > startIdx {
visUids = store.Uids()[startIdx : selectedIdx+1]
} else {
visUids = store.Uids()[selectedIdx : startIdx+1]
}
store.resetMark()
for _, uid := range visUids {
store.marked[uid] = struct{}{}
}
missing := make([]uint32, 0)
for _, uid := range visUids {
if msg := store.Messages[uid]; msg == nil {
missing = append(missing, uid)
}
}
store.FetchHeaders(missing, nil)
}
func (store *MessageStore) NextPrev(delta int) {
uids := store.Uids()
if len(uids) == 0 {
return
}
store.selected += delta
if store.selected < 0 {
store.selected = 0
}
if store.selected >= len(uids) {
store.selected = len(uids) - 1
}
store.updateVisual()
nextResultIndex := len(store.results) - store.resultIndex - 2*delta
if nextResultIndex < 0 || nextResultIndex >= len(store.results) {
return
}
nextResultUid := store.results[nextResultIndex]
selectedUid := uids[len(uids)-store.selected-1]
if nextResultUid == selectedUid {
store.resultIndex += delta
}
}
func (store *MessageStore) Next() {
store.NextPrev(1)
}
func (store *MessageStore) Prev() {
store.NextPrev(-1)
}
func (store *MessageStore) Search(args []string, cb func([]uint32)) {
store.worker.PostAction(&types.SearchDirectory{
Argv: args,
}, func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.SearchResults:
allowedUids := store.Uids()
uids := make([]uint32, 0, len(msg.Uids))
for _, uid := range msg.Uids {
for _, uidCheck := range allowedUids {
if uid == uidCheck {
uids = append(uids, uid)
break
}
}
}
sort.SortBy(uids, allowedUids)
cb(uids)
}
})
}
func (store *MessageStore) ApplySearch(results []uint32) {
store.results = results
store.resultIndex = -1
store.NextResult()
}
func (store *MessageStore) ApplyFilter(results []uint32) {
store.results = nil
store.filtered = results
store.filter = true
if store.onFilterChange != nil {
store.onFilterChange(store)
}
store.update()
// any marking is now invalid
// TODO: could save that probably
store.ClearVisualMark()
}
func (store *MessageStore) ApplyClear() {
store.results = nil
store.filtered = nil
store.filter = false
if store.BuildThreads() {
store.runThreadBuilder()
}
if store.onFilterChange != nil {
store.onFilterChange(store)
}
}
func (store *MessageStore) nextPrevResult(delta int) {
if len(store.results) == 0 {
return
}
store.resultIndex += delta
if store.resultIndex >= len(store.results) {
store.resultIndex = 0
}
if store.resultIndex < 0 {
store.resultIndex = len(store.results) - 1
}
uids := store.Uids()
for i, uid := range uids {
if store.results[len(store.results)-store.resultIndex-1] == uid {
store.Select(len(uids) - i - 1)
break
}
}
store.update()
}
func (store *MessageStore) NextResult() {
store.nextPrevResult(1)
}
func (store *MessageStore) PrevResult() {
store.nextPrevResult(-1)
}
func (store *MessageStore) ModifyLabels(uids []uint32, add, remove []string,
cb func(msg types.WorkerMessage)) {
store.worker.PostAction(&types.ModifyLabels{
Uids: uids,
Add: add,
Remove: remove,
}, cb)
}
func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func()) {
store.Sorting = true
store.sortCriteria = criteria
handle_return := func(msg types.WorkerMessage) {
store.Sorting = false
if cb != nil {
cb()
}
}
if store.thread {
store.worker.PostAction(&types.FetchDirectoryThreaded{
SortCriteria: criteria,
}, handle_return)
} else {
store.worker.PostAction(&types.FetchDirectoryContents{
SortCriteria: criteria,
}, handle_return)
}
}
// returns the index of needle in haystack or -1 if not found
func (store *MessageStore) visualStartIdx() int {
for idx, u := range store.Uids() {
if u == store.visualStartUid {
return idx
}
}
return -1
}