e50ab59284
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>
752 lines
17 KiB
Go
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
|
|
}
|