Make the message viewer real, part one
This commit is contained in:
parent
143289bbd0
commit
27b25174e2
7 changed files with 126 additions and 132 deletions
|
@ -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) {
|
||||||
|
|
27
commands/account/view-message.go
Normal file
27
commands/account/view-message.go
Normal 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
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue