Make the message viewer real, part one

This commit is contained in:
Drew DeVault 2019-03-31 12:14:37 -04:00
parent 143289bbd0
commit 27b25174e2
7 changed files with 126 additions and 132 deletions

View File

@ -20,9 +20,6 @@ func Pipe(aerc *widgets.Aerc, args []string) error {
return errors.New("Usage: :pipe <cmd> [args...]") return errors.New("Usage: :pipe <cmd> [args...]")
} }
acct := aerc.SelectedAccount() acct := aerc.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
store := acct.Messages().Store() store := acct.Messages().Store()
msg := acct.Messages().Selected() msg := acct.Messages().Selected()
store.FetchBodies([]uint32{msg.Uid}, func(reader io.Reader) { store.FetchBodies([]uint32{msg.Uid}, func(reader io.Reader) {

View File

@ -0,0 +1,27 @@
package account
import (
"errors"
"github.com/mattn/go-runewidth"
"git.sr.ht/~sircmpwn/aerc2/widgets"
)
func init() {
register("view-message", ViewMessage)
}
func ViewMessage(aerc *widgets.Aerc, args []string) error {
if len(args) != 1 {
return errors.New("Usage: view-message")
}
acct := aerc.SelectedAccount()
store := acct.Messages().Store()
msg := acct.Messages().Selected()
viewer := widgets.NewMessageViewer(store, msg)
aerc.NewTab(viewer, runewidth.Truncate(
msg.Envelope.Subject, 32, "…"))
return nil
}

View File

@ -67,7 +67,6 @@ func (store *MessageStore) FetchHeaders(uids []uint32,
} }
func (store *MessageStore) FetchBodies(uids []uint32, cb func(io.Reader)) { func (store *MessageStore) FetchBodies(uids []uint32, cb func(io.Reader)) {
// TODO: this could be optimized by pre-allocating toFetch and trimming it // 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. // at the end. In practice we expect to get most messages back in one frame.
var toFetch imap.SeqSet var toFetch imap.SeqSet
@ -89,6 +88,21 @@ func (store *MessageStore) FetchBodies(uids []uint32, cb func(io.Reader)) {
} }
} }
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.Reader)
})
}
func (store *MessageStore) merge( func (store *MessageStore) merge(
to *types.MessageInfo, from *types.MessageInfo) { to *types.MessageInfo, from *types.MessageInfo) {

View File

@ -62,8 +62,6 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
tabs.Add(view, acct.Name) tabs.Add(view, acct.Name)
} }
tabs.Add(NewMessageViewer(), "[PATCH todo.sr.ht v2 …")
return aerc return aerc
} }

View File

@ -2,124 +2,58 @@ package widgets
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"os/exec" "os/exec"
"github.com/emersion/go-imap"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"git.sr.ht/~sircmpwn/aerc2/lib"
"git.sr.ht/~sircmpwn/aerc2/lib/ui" "git.sr.ht/~sircmpwn/aerc2/lib/ui"
"git.sr.ht/~sircmpwn/aerc2/worker/types"
) )
type MessageViewer struct { type MessageViewer struct {
mail io.Reader
pipe io.Writer
grid *ui.Grid grid *ui.Grid
term *Terminal term *Terminal
} }
var testMsg = `Makes the following changes to the Event type: func formatAddresses(addrs []*imap.Address) string {
val := bytes.Buffer{}
for i, addr := range addrs {
if addr.PersonalName != "" {
val.WriteString(fmt.Sprintf("%s <%s@%s>",
addr.PersonalName, addr.MailboxName, addr.HostName))
} else {
val.WriteString(fmt.Sprintf("%s@%s",
addr.MailboxName, addr.HostName))
}
if i != len(addrs)-1 {
val.WriteString(", ")
}
}
return val.String()
}
* make 'user' and 'ticket' nullable since some events require it func NewMessageViewer(store *lib.MessageStore,
* add 'by_user' and 'from_ticket' to enable mentions msg *types.MessageInfo) *MessageViewer {
* remove 'assinged_user' which is no longer used
Ticket: https://todo.sr.ht/~sircmpwn/todo.sr.ht/156
---
tests/test_comments.py | 23 ++-
.../versions/75ff2f7624fd_new_event_fields.py | 142 ++++++++++++++++++
todosrht/templates/events.html | 18 ++-
todosrht/templates/ticket.html | 31 +++-
todosrht/tickets.py | 14 +-
todosrht/types/event.py | 16 +-
6 files changed, 207 insertions(+), 37 deletions(-)
create mode 100644 todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py
diff --git a/tests/test_comments.py b/tests/test_comments.py
index 4b3161d..b85d751 100644
--- a/tests/test_comments.py
+++ b/tests/test_comments.py
@@ -253,20 +253,25 @@ def test_notifications_and_events(mailbox):
# Check correct events are generated
comment_events = {e for e in ticket.events
if e.event_type == EventType.comment}
- user_events = {e for e in ticket.events
+ u1_events = {e for e in u1.events
+ if e.event_type == EventType.user_mentioned}
+ u2_events = {e for e in u2.events
if e.event_type == EventType.user_mentioned}
assert len(comment_events) == 1
- assert len(user_events) == 2
+ assert len(u1_events) == 1
+ assert len(u2_events) == 1
- u1_mention = next(e for e in user_events if e.user == u1)
- u2_mention = next(e for e in user_events if e.user == u2)
+ u1_mention = u1_events.pop()
+ u2_mention = u2_events.pop()
assert u1_mention.comment == comment
- assert u1_mention.ticket == ticket
+ assert u1_mention.from_ticket == ticket
+ assert u1_mention.by_user == commenter
assert u2_mention.comment == comment
- assert u2_mention.ticket == ticket
+ assert u2_mention.from_ticket == ticket
+ assert u2_mention.by_user == commenter
assert len(t1.events) == 1
assert len(t2.events) == 1
@@ -276,10 +281,12 @@ def test_notifications_and_events(mailbox):
t2_mention = t2.events[0]
assert t1_mention.comment == comment
- assert t1_mention.user == commenter
+ assert t1_mention.from_ticket == ticket
+ assert t1_mention.by_user == commenter
assert t2_mention.comment == comment
- assert t2_mention.user == commenter
+ assert t2_mention.from_ticket == ticket
+ assert t2_mention.by_user == commenter
def test_ticket_mention_pattern():
def match(text):
diff --git a/todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py
b/todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py
new file mode 100644
index 0000000..1c55bfe
--- /dev/null
+++ b/todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py
@@ -0,0 +1,142 @@
+"""Add new event fields and migrate data.
+
+Also makes Event.ticket_id and Event.user_id nullable since some these fields
+can be empty for mention events.
+
+Revision ID: 75ff2f7624fd
+Revises: c7146cb70d6b
+Create Date: 2019-03-28 16:26:18.714300
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "75ff2f7624fd"
+down_revision = "c7146cb70d6b"
`
func NewMessageViewer() *MessageViewer {
grid := ui.NewGrid().Rows([]ui.GridSpec{ grid := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_EXACT, 4}, {ui.SIZE_EXACT, 3}, // TODO: Based on number of header rows
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
}).Columns([]ui.GridSpec{ }).Columns([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
}) })
// TODO: let user specify additional headers to show by default
headers := ui.NewGrid().Rows([]ui.GridSpec{ headers := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_EXACT, 1}, {ui.SIZE_EXACT, 1},
{ui.SIZE_EXACT, 1}, {ui.SIZE_EXACT, 1},
{ui.SIZE_EXACT, 1}, {ui.SIZE_EXACT, 1},
{ui.SIZE_EXACT, 1},
}).Columns([]ui.GridSpec{ }).Columns([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
@ -127,25 +61,19 @@ func NewMessageViewer() *MessageViewer {
headers.AddChild( headers.AddChild(
&HeaderView{ &HeaderView{
Name: "From", Name: "From",
Value: "Ivan Habunek <ivan@habunek.com>", Value: formatAddresses(msg.Envelope.From),
}).At(0, 0) }).At(0, 0)
headers.AddChild( headers.AddChild(
&HeaderView{ &HeaderView{
Name: "To", Name: "To",
Value: "~sircmpwn/sr.ht-dev@lists.sr.ht", Value: formatAddresses(msg.Envelope.To),
}).At(0, 1) }).At(0, 1)
headers.AddChild( headers.AddChild(
&HeaderView{ &HeaderView{
Name: "Subject", Name: "Subject",
Value: "[PATCH todo.sr.ht v2 1/3 Alter Event fields " + Value: msg.Envelope.Subject,
"and migrate data]",
}).At(1, 0).Span(1, 2) }).At(1, 0).Span(1, 2)
headers.AddChild( headers.AddChild(ui.NewFill(' ')).At(2, 0).Span(1, 2)
&HeaderView{
Name: "PGP",
Value: "✓ Valid PGP signature from Ivan Habunek",
}).At(2, 0).Span(1, 2)
headers.AddChild(ui.NewFill(' ')).At(3, 0).Span(1, 2)
body := ui.NewGrid().Rows([]ui.GridSpec{ body := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
@ -154,25 +82,30 @@ func NewMessageViewer() *MessageViewer {
{ui.SIZE_EXACT, 20}, {ui.SIZE_EXACT, 20},
}) })
cmd := exec.Command("sh", "-c", "./contrib/hldiff.py | less -R") cmd := exec.Command("less")
pipe, _ := cmd.StdinPipe() pipe, _ := cmd.StdinPipe()
term, _ := NewTerminal(cmd) term, _ := NewTerminal(cmd)
term.OnStart = func() { // TODO: configure multipart view. I left a spot for it in the grid
go func() { body.AddChild(term).At(0, 0).Span(1, 2)
reader := bytes.NewBufferString(testMsg)
io.Copy(pipe, reader)
pipe.Close()
}()
}
term.Focus(true)
body.AddChild(term).At(0, 0)
body.AddChild(ui.NewBordered(
&MultipartView{}, ui.BORDER_LEFT)).At(0, 1)
grid.AddChild(headers).At(0, 0) grid.AddChild(headers).At(0, 0)
grid.AddChild(body).At(1, 0) grid.AddChild(body).At(1, 0)
return &MessageViewer{grid, term}
viewer := &MessageViewer{
pipe: pipe,
grid: grid,
term: term,
}
store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) {
viewer.mail = reader
go func() {
io.Copy(pipe, reader)
pipe.Close()
}()
})
return viewer
} }
func (mv *MessageViewer) Draw(ctx *ui.Context) { func (mv *MessageViewer) Draw(ctx *ui.Context) {
@ -205,7 +138,10 @@ type HeaderView struct {
} }
func (hv *HeaderView) Draw(ctx *ui.Context) { func (hv *HeaderView) Draw(ctx *ui.Context) {
size := runewidth.StringWidth(hv.Name) name := hv.Name
size := runewidth.StringWidth(name)
lim := ctx.Width() - size - 1
value := runewidth.Truncate(" "+hv.Value, lim, "…")
var ( var (
hstyle tcell.Style hstyle tcell.Style
vstyle tcell.Style vstyle tcell.Style
@ -219,8 +155,8 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
hstyle = tcell.StyleDefault.Bold(true) hstyle = tcell.StyleDefault.Bold(true)
} }
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle) ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
ctx.Printf(0, 0, hstyle, hv.Name) ctx.Printf(0, 0, hstyle, name)
ctx.Printf(size, 0, vstyle, " "+hv.Value) ctx.Printf(size, 0, vstyle, value)
} }
func (hv *HeaderView) Invalidate() { func (hv *HeaderView) Invalidate() {

View File

@ -30,6 +30,18 @@ func (imapw *IMAPWorker) handleFetchMessageBodies(
imapw.handleFetchMessages(msg, &msg.Uids, items) imapw.handleFetchMessages(msg, &msg.Uids, items)
} }
func (imapw *IMAPWorker) handleFetchMessageBodyPart(
msg *types.FetchMessageBodyPart) {
imapw.worker.Logger.Printf("Fetching message part")
section := &imap.BodySectionName{}
section.Path = []int{msg.Part}
items := []imap.FetchItem{section.FetchItem()}
uids := imap.SeqSet{}
uids.AddNum(msg.Uid)
imapw.handleFetchMessages(msg, &uids, items)
}
func (imapw *IMAPWorker) handleFetchMessages( func (imapw *IMAPWorker) handleFetchMessages(
msg types.WorkerMessage, uids *imap.SeqSet, items []imap.FetchItem) { msg types.WorkerMessage, uids *imap.SeqSet, items []imap.FetchItem) {
@ -43,12 +55,8 @@ func (imapw *IMAPWorker) handleFetchMessages(
section := &imap.BodySectionName{} section := &imap.BodySectionName{}
for _msg := range messages { for _msg := range messages {
imapw.seqMap[_msg.SeqNum-1] = _msg.Uid imapw.seqMap[_msg.SeqNum-1] = _msg.Uid
if reader := _msg.GetBody(section); reader != nil { switch msg.(type) {
imapw.worker.PostMessage(&types.MessageBody{ case *types.FetchMessageHeaders:
Reader: reader,
Uid: _msg.Uid,
}, nil)
} else {
imapw.worker.PostMessage(&types.MessageInfo{ imapw.worker.PostMessage(&types.MessageInfo{
BodyStructure: _msg.BodyStructure, BodyStructure: _msg.BodyStructure,
Envelope: _msg.Envelope, Envelope: _msg.Envelope,
@ -56,6 +64,18 @@ func (imapw *IMAPWorker) handleFetchMessages(
InternalDate: _msg.InternalDate, InternalDate: _msg.InternalDate,
Uid: _msg.Uid, Uid: _msg.Uid,
}, nil) }, nil)
case *types.FetchMessageBodies:
reader := _msg.GetBody(section)
imapw.worker.PostMessage(&types.MessageBody{
Reader: reader,
Uid: _msg.Uid,
}, nil)
case *types.FetchMessageBodyPart:
reader := _msg.GetBody(section)
imapw.worker.PostMessage(&types.MessageBodyPart{
Reader: reader,
Uid: _msg.Uid,
}, nil)
} }
} }
if err := <-done; err != nil { if err := <-done; err != nil {

View File

@ -160,6 +160,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
w.handleFetchMessageHeaders(msg) w.handleFetchMessageHeaders(msg)
case *types.FetchMessageBodies: case *types.FetchMessageBodies:
w.handleFetchMessageBodies(msg) w.handleFetchMessageBodies(msg)
case *types.FetchMessageBodyPart:
w.handleFetchMessageBodyPart(msg)
case *types.DeleteMessages: case *types.DeleteMessages:
w.handleDeleteMessages(msg) w.handleDeleteMessages(msg)
default: default: