From cfc19a7ec22a1c60f79427ddbabdf437705efbab Mon Sep 17 00:00:00 2001 From: Koni Marti Date: Mon, 8 Aug 2022 22:21:41 +0200 Subject: [PATCH] store: extract marking behavior and add tests Separate the marking functions from the message store and extract the marking behavior into its own class with tests. Signed-off-by: Koni Marti Acked-by: Robin Jarry --- commands/msg/archive.go | 5 +- commands/msg/copy.go | 2 +- commands/msg/delete.go | 7 +- commands/msg/mark.go | 15 +-- commands/msg/modify-labels.go | 2 +- commands/msg/move.go | 5 +- commands/msg/pipe.go | 2 +- commands/msg/read.go | 2 +- lib/marker/marker.go | 179 ++++++++++++++++++++++++++++++++++ lib/marker/marker_test.go | 139 ++++++++++++++++++++++++++ lib/msgstore.go | 175 +++++---------------------------- widgets/account.go | 8 +- widgets/msglist.go | 6 +- widgets/msgviewer.go | 3 +- 14 files changed, 373 insertions(+), 177 deletions(-) create mode 100644 lib/marker/marker.go create mode 100644 lib/marker/marker_test.go diff --git a/commands/msg/archive.go b/commands/msg/archive.go index b8d8ff6..f359f43 100644 --- a/commands/msg/archive.go +++ b/commands/msg/archive.go @@ -57,7 +57,8 @@ func (Archive) Execute(aerc *widgets.Aerc, args []string) error { for _, msg := range msgs { uids = append(uids, msg.Uid) } - store.ClearVisualMark() + marker := store.Marker() + marker.ClearVisualMark() findNextNonDeleted(uids, store) var uidMap map[string][]uint32 @@ -95,7 +96,7 @@ func (Archive) Execute(aerc *widgets.Aerc, args []string) error { aerc.PushError(msg.Error.Error()) success = false wg.Done() - store.Remark() + marker.Remark() } }) } diff --git a/commands/msg/copy.go b/commands/msg/copy.go index 6a106cf..7118e4f 100644 --- a/commands/msg/copy.go +++ b/commands/msg/copy.go @@ -56,7 +56,7 @@ func (Copy) Execute(aerc *widgets.Aerc, args []string) error { switch msg := msg.(type) { case *types.Done: aerc.PushStatus("Messages copied.", 10*time.Second) - store.ClearVisualMark() + store.Marker().ClearVisualMark() case *types.Error: aerc.PushError(msg.Error.Error()) } diff --git a/commands/msg/delete.go b/commands/msg/delete.go index ceb570b..9db850e 100644 --- a/commands/msg/delete.go +++ b/commands/msg/delete.go @@ -43,9 +43,10 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error { return err } sel := store.Selected() + marker := store.Marker() + marker.ClearVisualMark() // caution, can be nil next := findNextNonDeleted(uids, store) - store.ClearVisualMark() store.Delete(uids, func(msg types.WorkerMessage) { switch msg := msg.(type) { case *types.Done: @@ -80,11 +81,11 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error { } } case *types.Error: - store.Remark() + marker.Remark() store.Select(sel.Uid) aerc.PushError(msg.Error.Error()) case *types.Unsupported: - store.Remark() + marker.Remark() store.Select(sel.Uid) // notmuch doesn't support it, we want the user to know aerc.PushError(" error, unsupported for this worker") diff --git a/commands/msg/mark.go b/commands/msg/mark.go index e15a9f6..13d6c92 100644 --- a/commands/msg/mark.go +++ b/commands/msg/mark.go @@ -31,6 +31,7 @@ func (Mark) Execute(aerc *widgets.Aerc, args []string) error { if err != nil { return err } + marker := store.Marker() opts, _, err := getopt.Getopts(args, "atv") if err != nil { return err @@ -57,9 +58,9 @@ func (Mark) Execute(aerc *widgets.Aerc, args []string) error { var modFunc func(uint32) if toggle { - modFunc = store.ToggleMark + modFunc = marker.ToggleMark } else { - modFunc = store.Mark + modFunc = marker.Mark } switch { case all: @@ -69,7 +70,7 @@ func (Mark) Execute(aerc *widgets.Aerc, args []string) error { } return nil case visual: - store.ToggleVisualMark() + marker.ToggleVisualMark() return nil default: modFunc(selected.Uid) @@ -85,21 +86,21 @@ func (Mark) Execute(aerc *widgets.Aerc, args []string) error { case all && toggle: uids := store.Uids() for _, uid := range uids { - store.ToggleMark(uid) + marker.ToggleMark(uid) } return nil case all && !toggle: - store.ClearVisualMark() + marker.ClearVisualMark() return nil default: - store.Unmark(selected.Uid) + marker.Unmark(selected.Uid) return nil } case "remark": if all || visual || toggle { return fmt.Errorf("Usage: :remark") } - store.Remark() + marker.Remark() return nil } return nil // never reached diff --git a/commands/msg/modify-labels.go b/commands/msg/modify-labels.go index 655004c..d0a5d6f 100644 --- a/commands/msg/modify-labels.go +++ b/commands/msg/modify-labels.go @@ -57,7 +57,7 @@ func (ModifyLabels) Execute(aerc *widgets.Aerc, args []string) error { switch msg := msg.(type) { case *types.Done: aerc.PushStatus("labels updated", 10*time.Second) - store.ClearVisualMark() + store.Marker().ClearVisualMark() case *types.Error: aerc.PushError(msg.Error.Error()) } diff --git a/commands/msg/move.go b/commands/msg/move.go index 6eca667..13b1fb7 100644 --- a/commands/msg/move.go +++ b/commands/msg/move.go @@ -54,7 +54,8 @@ func (Move) Execute(aerc *widgets.Aerc, args []string) error { if isMsgView { aerc.RemoveTab(h.msgProvider) } - store.ClearVisualMark() + marker := store.Marker() + marker.ClearVisualMark() findNextNonDeleted(uids, store) joinedArgs := strings.Join(args[optind:], " ") store.Move(uids, joinedArgs, createParents, func( @@ -64,7 +65,7 @@ func (Move) Execute(aerc *widgets.Aerc, args []string) error { case *types.Done: aerc.PushStatus("Message moved to "+joinedArgs, 10*time.Second) case *types.Error: - store.Remark() + marker.Remark() aerc.PushError(msg.Error.Error()) } }) diff --git a/commands/msg/pipe.go b/commands/msg/pipe.go index 7a2489a..29d63ba 100644 --- a/commands/msg/pipe.go +++ b/commands/msg/pipe.go @@ -218,7 +218,7 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error { } }) } - provider.Store().ClearVisualMark() + provider.Store().Marker().ClearVisualMark() return nil } diff --git a/commands/msg/read.go b/commands/msg/read.go index e4d091f..957b7d7 100644 --- a/commands/msg/read.go +++ b/commands/msg/read.go @@ -175,7 +175,7 @@ func (FlagMsg) Execute(aerc *widgets.Aerc, args []string) error { wg.Wait() if success { aerc.PushStatus(actionName+" flag '"+flagName+"' successful", 10*time.Second) - store.ClearVisualMark() + store.Marker().ClearVisualMark() } }() diff --git a/lib/marker/marker.go b/lib/marker/marker.go new file mode 100644 index 0000000..1b89fc8 --- /dev/null +++ b/lib/marker/marker.go @@ -0,0 +1,179 @@ +package marker + +// TODO: fix headers for message that are nil + +// Marker provides the interface for the marking behavior of messages +type Marker interface { + Mark(uint32) + Unmark(uint32) + ToggleMark(uint32) + Remark() + Marked() []uint32 + IsMarked(uint32) bool + ToggleVisualMark() + UpdateVisualMark() + ClearVisualMark() +} + +// UIDProvider provides the underlying uids and the selected message index +type UIDProvider interface { + Uids() []uint32 + SelectedIndex() int +} + +type controller struct { + uidProvider UIDProvider + marked map[uint32]struct{} + lastMarked map[uint32]struct{} + visualStartUID uint32 + visualMarkMode bool +} + +// New returns a new Marker +func New(up UIDProvider) Marker { + return &controller{ + uidProvider: up, + marked: make(map[uint32]struct{}), + lastMarked: make(map[uint32]struct{}), + } +} + +// Mark markes the uid as marked +func (mc *controller) Mark(uid uint32) { + if mc.visualMarkMode { + // visual mode has override, bogus input from user + return + } + mc.marked[uid] = struct{}{} +} + +// Unmark unmarks the uid +func (mc *controller) Unmark(uid uint32) { + if mc.visualMarkMode { + // user probably wanted to clear the visual marking + mc.ClearVisualMark() + return + } + delete(mc.marked, uid) +} + +// Remark restores the previous marks +func (mc *controller) Remark() { + mc.marked = mc.lastMarked +} + +// ToggleMark toggles the marked state for the given uid +func (mc *controller) ToggleMark(uid uint32) { + if mc.visualMarkMode { + // visual mode has override, bogus input from user + return + } + if mc.IsMarked(uid) { + mc.Unmark(uid) + } else { + mc.Mark(uid) + } +} + +// resetMark removes the marking from all messages +func (mc *controller) resetMark() { + mc.lastMarked = mc.marked + mc.marked = make(map[uint32]struct{}) +} + +// removeStaleUID removes uids that are no longer presents in the UIDProvider +func (mc *controller) removeStaleUID() { + for mark := range mc.marked { + present := false + for _, uid := range mc.uidProvider.Uids() { + if mark == uid { + present = true + break + } + } + if !present { + delete(mc.marked, mark) + } + } +} + +// IsMarked checks whether the given uid has been marked +func (mc *controller) IsMarked(uid uint32) bool { + _, marked := mc.marked[uid] + return marked +} + +// Marked returns the uids of all marked messages +func (mc *controller) Marked() []uint32 { + mc.removeStaleUID() + marked := make([]uint32, len(mc.marked)) + i := 0 + for uid := range mc.marked { + marked[i] = uid + i++ + } + return marked +} + +// ToggleVisualMark enters or leaves the visual marking mode +func (mc *controller) ToggleVisualMark() { + mc.visualMarkMode = !mc.visualMarkMode + if mc.visualMarkMode { + // just entered visual mode, reset whatever marking was already done + mc.resetMark() + uids := mc.uidProvider.Uids() + if idx := mc.uidProvider.SelectedIndex(); idx >= 0 && idx < len(uids) { + mc.visualStartUID = uids[idx] + mc.marked[mc.visualStartUID] = struct{}{} + } + } +} + +// ClearVisualMark leaves the visual marking mode and resets any marking +func (mc *controller) ClearVisualMark() { + mc.resetMark() + mc.visualMarkMode = false + mc.visualStartUID = 0 +} + +// UpdateVisualMark updates the index with the currently selected message +func (mc *controller) UpdateVisualMark() { + if !mc.visualMarkMode { + // nothing to do + return + } + startIdx := mc.visualStartIdx() + if startIdx < 0 { + // something deleted the startuid, abort the marking process + mc.ClearVisualMark() + return + } + + selectedIdx := mc.uidProvider.SelectedIndex() + if selectedIdx < 0 { + return + } + + uids := mc.uidProvider.Uids() + + var visUids []uint32 + if selectedIdx > startIdx { + visUids = uids[startIdx : selectedIdx+1] + } else { + visUids = uids[selectedIdx : startIdx+1] + } + mc.resetMark() + for _, uid := range visUids { + mc.marked[uid] = struct{}{} + } +} + +// returns the index of needle in haystack or -1 if not found +func (mc *controller) visualStartIdx() int { + for idx, u := range mc.uidProvider.Uids() { + if u == mc.visualStartUID { + return idx + } + } + return -1 +} diff --git a/lib/marker/marker_test.go b/lib/marker/marker_test.go new file mode 100644 index 0000000..1611623 --- /dev/null +++ b/lib/marker/marker_test.go @@ -0,0 +1,139 @@ +package marker_test + +import ( + "testing" + + "git.sr.ht/~rjarry/aerc/lib/marker" +) + +// mockUidProvider implements the UidProvider interface and mocks the message +// store for testing +type mockUidProvider struct { + uids []uint32 + idx int +} + +func (mock *mockUidProvider) Uids() []uint32 { + return mock.uids +} + +func (mock *mockUidProvider) SelectedIndex() int { + return mock.idx +} + +func createMarker() (marker.Marker, *mockUidProvider) { + uidProvider := &mockUidProvider{ + uids: []uint32{1, 2, 3, 4}, + idx: 1, + } + m := marker.New(uidProvider) + return m, uidProvider +} + +func TestMarker_MarkUnmark(t *testing.T) { + m, _ := createMarker() + uid := uint32(4) + + m.Mark(uid) + if !m.IsMarked(uid) { + t.Errorf("Marking failed") + } + + m.Unmark(uid) + if m.IsMarked(uid) { + t.Errorf("Unmarking failed") + } +} + +func TestMarker_ToggleMark(t *testing.T) { + m, _ := createMarker() + uid := uint32(4) + + if m.IsMarked(uid) { + t.Errorf("ToggleMark: uid should not be marked") + } + + m.ToggleMark(uid) + if !m.IsMarked(uid) { + t.Errorf("ToggleMark: uid should be marked") + } + + m.ToggleMark(uid) + if m.IsMarked(uid) { + t.Errorf("ToggleMark: uid should not be marked") + } +} + +func TestMarker_Marked(t *testing.T) { + m, _ := createMarker() + expected := map[uint32]struct{}{ + uint32(1): {}, + uint32(4): {}, + } + for uid := range expected { + m.Mark(uid) + } + + got := m.Marked() + if len(expected) != len(got) { + t.Errorf("Marked: expected len of %d but got %d", len(expected), len(got)) + } + + for _, uid := range got { + if _, ok := expected[uid]; !ok { + t.Errorf("Marked: received uid %d as marked but it should not be", uid) + } + } +} + +func TestMarker_VisualMode(t *testing.T) { + m, up := createMarker() + + // activate visual mode + m.ToggleVisualMark() + + // marking should now fail silently because we're in visual mode + m.Mark(1) + if m.IsMarked(1) { + t.Errorf("marking in visual mode should not work") + } + + // move selection index to last item + up.idx = len(up.uids) - 1 + m.UpdateVisualMark() + expectedMarked := []uint32{2, 3, 4} + + for _, uidMarked := range expectedMarked { + if !m.IsMarked(uidMarked) { + t.Logf("expected: %#v, got: %#v", expectedMarked, m.Marked()) + t.Errorf("updatevisual: uid %v should be marked in visual mode", uidMarked) + } + } + + // clear all + m.ClearVisualMark() + if len(m.Marked()) > 0 { + t.Errorf("no uids should be marked after clearing visual mark") + } + + // test remark + m.Remark() + for _, uidMarked := range expectedMarked { + if !m.IsMarked(uidMarked) { + t.Errorf("remark: uid %v should be marked in visual mode", uidMarked) + } + } +} + +func TestMarker_MarkOutOfBound(t *testing.T) { + m, _ := createMarker() + + outOfBoundUid := uint32(100) + + m.Mark(outOfBoundUid) + for _, markedUid := range m.Marked() { + if markedUid == outOfBoundUid { + t.Errorf("out-of-bound uid should not be marked") + } + } +} diff --git a/lib/msgstore.go b/lib/msgstore.go index 9799a99..f9b68ad 100644 --- a/lib/msgstore.go +++ b/lib/msgstore.go @@ -5,6 +5,7 @@ import ( "sync" "time" + "git.sr.ht/~rjarry/aerc/lib/marker" "git.sr.ht/~rjarry/aerc/lib/sort" "git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/models" @@ -27,10 +28,7 @@ type MessageStore struct { headerCallbacks map[uint32][]func(*types.MessageInfo) // marking - marked map[uint32]struct{} - lastMarked map[uint32]struct{} - visualStartUid uint32 - visualMarkMode bool + marker marker.Marker // Search/filter results results []uint32 @@ -78,8 +76,8 @@ func NewMessageStore(worker *types.Worker, DirInfo: *dirInfo, Messages: make(map[uint32]*models.MessageInfo), - selectedUid: MagicUid, - marked: make(map[uint32]struct{}), + selectedUid: MagicUid, + bodyCallbacks: make(map[uint32][]func(*types.FullMessage)), headerCallbacks: make(map[uint32][]func(*types.MessageInfo)), @@ -215,7 +213,6 @@ func (store *MessageStore) Update(msg types.WorkerMessage) { } store.Messages = newMap store.uids = msg.Uids - store.checkMark() update = true case *types.DirectoryThreaded: var uids []uint32 @@ -236,7 +233,6 @@ func (store *MessageStore) Update(msg types.WorkerMessage) { } store.Messages = newMap store.uids = uids - store.checkMark() store.threads = msg.Threads update = true case *types.MessageInfo: @@ -290,7 +286,6 @@ func (store *MessageStore) Update(msg types.WorkerMessage) { 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 @@ -539,142 +534,9 @@ func (store *MessageStore) SelectedUid() uint32 { func (store *MessageStore) Select(uid uint32) { store.selectedUid = uid - 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 + if store.marker != nil { + store.marker.UpdateVisualMark() } - 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) -} - -func (store *MessageStore) Remark() { - store.marked = store.lastMarked -} - -// 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.lastMarked = store.marked - store.marked = make(map[uint32]struct{}) -} - -// checkMark checks that no stale uids remain marked -func (store *MessageStore) checkMark() { - for mark := range store.marked { - present := false - for _, uid := range store.uids { - if mark == uid { - present = true - break - } - } - if !present { - delete(store.marked, mark) - } - } -} - -// 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 - } - - selectedIdx := store.FindIndexByUid(store.SelectedUid()) - if selectedIdx < 0 { - store.ClearVisualMark() - return - } - - 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) { @@ -700,7 +562,9 @@ func (store *MessageStore) NextPrev(delta int) { store.Select(uids[newIdx]) - store.updateVisual() + if store.marker != nil { + store.marker.UpdateVisualMark() + } nextResultIndex := len(store.results) - store.resultIndex - 2*delta if nextResultIndex < 0 || nextResultIndex >= len(store.results) { @@ -821,14 +685,15 @@ func (store *MessageStore) GetCurrentSortCriteria() []*types.SortCriterion { return store.sortCriteria } -// 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 - } +func (store *MessageStore) SetMarker(m marker.Marker) { + store.marker = m +} + +func (store *MessageStore) Marker() marker.Marker { + if store.marker == nil { + store.marker = marker.New(store) } - return -1 + return store.marker } // FindIndexByUid returns the index in store.Uids() or -1 if not found @@ -845,3 +710,9 @@ func (store *MessageStore) FindIndexByUid(uid uint32) int { func (store *MessageStore) Capabilities() *models.Capabilities { return store.DirInfo.Caps } + +// SelectedIndex returns the index of the selected message in the uid list or +// -1 if not found +func (store *MessageStore) SelectedIndex() int { + return store.FindIndexByUid(store.selectedUid) +} diff --git a/widgets/account.go b/widgets/account.go index ceb8d98..b92a619 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -9,6 +9,7 @@ import ( "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" + "git.sr.ht/~rjarry/aerc/lib/marker" "git.sr.ht/~rjarry/aerc/lib/sort" "git.sr.ht/~rjarry/aerc/lib/statusline" "git.sr.ht/~rjarry/aerc/lib/ui" @@ -222,8 +223,10 @@ func (acct *AccountView) SelectedMessage() (*models.MessageInfo, error) { } func (acct *AccountView) MarkedMessages() ([]uint32, error) { - store := acct.Store() - return store.Marked(), nil + if store := acct.Store(); store != nil { + return store.Marker().Marked(), nil + } + return nil, errors.New("no store available") } func (acct *AccountView) SelectedMessagePart() *PartInfo { @@ -301,6 +304,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) { acct.host.Beep() } }) + store.SetMarker(marker.New(store)) acct.dirlist.SetMsgStore(msg.Info.Name, store) } case *types.DirectoryContents: diff --git a/widgets/msglist.go b/widgets/msglist.go index e431c2e..e505608 100644 --- a/widgets/msglist.go +++ b/widgets/msglist.go @@ -122,7 +122,7 @@ func (ml *MessageList) Draw(ctx *ui.Context) { AccountName: acct.Name(), MsgInfo: msg, MsgNum: row, - MsgIsMarked: store.IsMarked(t.Uid), + MsgIsMarked: store.Marker().IsMarked(t.Uid), ThreadPrefix: prefix, ThreadSameSubject: normalizedSubject == lastSubject, } @@ -150,7 +150,7 @@ func (ml *MessageList) Draw(ctx *ui.Context) { AccountName: acct.Name(), MsgInfo: msg, MsgNum: row, - MsgIsMarked: store.IsMarked(uid), + MsgIsMarked: store.Marker().IsMarked(uid), } if ml.drawRow(textWidth, ctx, uid, row, &needsHeaders, fmtCtx) { break @@ -244,7 +244,7 @@ func (ml *MessageList) drawRow(textWidth int, ctx *ui.Context, uid uint32, row i } // marked message - if store.IsMarked(msg.Uid) { + if store.Marker().IsMarked(msg.Uid) { msg_styles = append(msg_styles, config.STYLE_MSGLIST_MARKED) } diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go index 70ff5d8..cd0c937 100644 --- a/widgets/msgviewer.go +++ b/widgets/msgviewer.go @@ -302,8 +302,7 @@ func (mv *MessageViewer) SelectedMessage() (*models.MessageInfo, error) { } func (mv *MessageViewer) MarkedMessages() ([]uint32, error) { - store := mv.Store() - return store.Marked(), nil + return mv.acct.MarkedMessages() } func (mv *MessageViewer) ToggleHeaders() {