Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions internal/tui/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@ func (v *mailView) HelpBindings() []helpBinding {
if v.inThread {
bindings := []helpBinding{{"r", "reply"}, {"f", "forward"}}
if v.fileablePosting() != nil {
bindings = append(bindings, helpBinding{"l", "reply later"}, helpBinding{"a", "set aside"})
bindings = append(bindings, helpBinding{"l", "reply later"}, helpBinding{"a", "set aside"}, helpBinding{"t", "trash"})
}
if len(v.entries) > 1 {
bindings = append(bindings, helpBinding{"j/k", "next/previous message"})
Expand Down Expand Up @@ -1266,7 +1266,7 @@ func (v *mailView) HandleContentKey(msg tea.KeyPressMsg) tea.Cmd {
if v.topicID != 0 {
return v.loadForwardContext(v.topicID, v.topicName)
}
case "a", "A", "l":
case "a", "A", "l", "t", "T":
return v.fileOpenThread(msg.String())
case "[":
v.moveAttachmentCursor(-1)
Expand Down Expand Up @@ -2326,6 +2326,12 @@ func (v *mailView) fileOpenThread(key string) tea.Cmd {
if move == nil {
return nil
}
// Set Aside and Reply Later leave the thread on screen, in the box it landed
// in, so the next filing key can act on it there. Trash closes it: the Trash
// is not a box you file out of, and the web app returns to the list too.
if key == "t" || key == "T" {
v.ExitThread()
}
// Filing keys pressed faster than their requests answer can complete out of
// order, so each dispatch takes a sequence number and only the latest one
// records where the thread landed.
Expand Down
127 changes: 127 additions & 0 deletions internal/tui/mail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2409,6 +2409,120 @@ func TestMailViewContentKeyInThread(t *testing.T) {
v.HandleContentKey(keyPress("up"))
}

func TestMailViewTrashesTheOpenThreadAndReturnsToTheList(t *testing.T) {
for _, key := range []string{"t", "T"} {
t.Run(key, func(t *testing.T) {
v, recorded := mailWithTestServer(t, http.StatusNoContent)

v.Update(runCmd(v.HandleContentKey(keyPress("enter"))))
if !v.inThread || v.threadPosting.ID != 100 {
t.Fatalf("thread state = open:%v posting:%d", v.inThread, v.threadPosting.ID)
}

cmd := v.HandleContentKey(keyPress(key))
if v.inThread {
t.Error("trashing the open thread should return to the list")
}
done, ok := runCmd(cmd).(postingActionDoneMsg)
if !ok || done.err != nil {
t.Fatalf("trash command returned %#v", done)
}
if done.postingID != 100 || done.effect != postingActionRemove {
t.Errorf("action = posting %d effect %v, want posting 100 effect %v", done.postingID, done.effect, postingActionRemove)
}
if recorded.method != http.MethodPost || recorded.path != "/postings/trash.json" {
t.Errorf("request = %s %s, want POST /postings/trash.json", recorded.method, recorded.path)
}
if len(recorded.body.PostingIDs) != 1 || recorded.body.PostingIDs[0] != 100 {
t.Errorf("posting_ids = %v, want [100]", recorded.body.PostingIDs)
}

answer, _ := v.Update(done)
if toast := deliverToView(v, answer); toast != "Thread moved to Trash" {
t.Errorf("toast = %q, want %q", toast, "Thread moved to Trash")
}
if len(v.postingList.postings) != 1 || v.postingList.postings[0].ID != 101 {
t.Errorf("postings after trashing = %v, want the other thread alone", v.postingList.postings)
}
})
}
}

// Trash keeps to the same rule as the other filing keys: a thread opened over
// search results or a bundle has no row to file, so the key says so rather than
// trashing whatever the box list's cursor happens to be sitting on.
func TestMailViewRefusesToTrashAThreadOpenedFromSearchResults(t *testing.T) {
v, recorded := mailWithTestServer(t, http.StatusNoContent)
v.searchActive = true
v.searchQuery = "quarterly planning"
v.searchList.setPostings([]mail.Posting{{ID: 10, TopicID: 100, Name: "Hello world"}})

v.Update(runCmd(v.HandleContentKey(keyPress("enter"))))
if !v.inThread {
t.Fatal("the searched thread should have opened")
}

if cmd := v.HandleContentKey(keyPress("t")); cmd != nil {
t.Errorf("trash over search results returned %#v, want nothing", runCmd(cmd))
}
if !v.inThread {
t.Error("a refused trash should leave the thread open")
}
if v.notice == "" {
t.Error("a refused trash should say why")
}
if recorded.path == "/postings/trash.json" {
t.Error("a refused trash still asked the server to trash something")
}
}

func TestMailViewCannotTrashAThreadOpenedWithoutItsPosting(t *testing.T) {
v, recorded := mailWithTestServer(t, http.StatusNoContent)
v.Update(topicLoadedMsg{
topicID: 100,
title: "Hello world",
entries: []mail.Entry{{Creator: mail.Contact{Name: "Alice"}, Body: htmlutil.ToMarkdown("<p>hello</p>")}},
})

if cmd := v.HandleContentKey(keyPress("t")); cmd != nil {
t.Errorf("trash without a posting returned %#v, want nothing", runCmd(cmd))
}
if !v.inThread {
t.Error("a refused trash should leave the thread open")
}
if v.notice == "" {
t.Error("a refused trash should say why")
}
if recorded.method != "" {
t.Errorf("a refused trash sent %s %s", recorded.method, recorded.path)
}
}

func TestMailViewTrashesAThreadOpenedFromPreviouslySeen(t *testing.T) {
v, _ := mailWithTestServer(t, http.StatusNoContent)
loaded, _ := runCmd(v.handleBoxShortcut("9")).(seenLoadedMsg)
v.Update(loaded)
v.Update(runCmd(v.HandleContentKey(keyPress("enter"))))
if !v.inThread || !v.seenActive || v.threadPosting.ID != 611 {
t.Fatalf("thread state = open:%v seen:%v posting:%d", v.inThread, v.seenActive, v.threadPosting.ID)
}

done, ok := runCmd(v.HandleContentKey(keyPress("t"))).(postingActionDoneMsg)
if !ok || done.err != nil || !done.seen {
t.Fatalf("trash command returned %#v", done)
}
if v.inThread || !v.seenActive {
t.Errorf("trashing landed on open:%v seen:%v, want the Previously Seen list", v.inThread, v.seenActive)
}
v.Update(done)
if len(v.seenList.postings) != 0 {
t.Errorf("seen postings after trashing = %+v, want the row gone", v.seenList.postings)
}
if len(v.postingList.postings) != 2 {
t.Errorf("a seen-screen trash landed on the box list: %+v", v.postingList.postings)
}
}

// --- Subnav ---

func TestMailViewSubnavItems(t *testing.T) {
Expand Down Expand Up @@ -3080,6 +3194,19 @@ func TestMailViewHelpBindings(t *testing.T) {
}
}

func TestMailViewOpenThreadHelpOffersTrashOnlyWithAPosting(t *testing.T) {
v, _ := mailWithTestServer(t, http.StatusNoContent)
v.Update(runCmd(v.HandleContentKey(keyPress("enter"))))
if !hasHelpBinding(v.HelpBindings(), "t") {
t.Errorf("help bindings = %v, want trash among them", v.HelpBindings())
}

v.threadPosting = mail.Posting{}
if hasHelpBinding(v.HelpBindings(), "t") {
t.Errorf("help bindings = %v, want no trash without a posting", v.HelpBindings())
}
}

// A label scrolls rather than paging, so it advertises no page keys and p keeps meaning
// paper trail.
func TestMailViewLabelHelpOffersNoPageKeys(t *testing.T) {
Expand Down
Loading