~sircmpwn/aerc

27b25174e2f0249a6a1d4ba45b70f8504b63ffb1 — Drew DeVault 2 years ago 143289b
Make the message viewer real, part one
M commands/account/pipe.go => commands/account/pipe.go +0 -3
@@ 20,9 20,6 @@ func Pipe(aerc *widgets.Aerc, args []string) error {
		return errors.New("Usage: :pipe <cmd> [args...]")
	}
	acct := aerc.SelectedAccount()
	if acct == nil {
		return errors.New("No account selected")
	}
	store := acct.Messages().Store()
	msg := acct.Messages().Selected()
	store.FetchBodies([]uint32{msg.Uid}, func(reader io.Reader) {

A commands/account/view-message.go => commands/account/view-message.go +27 -0
@@ 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
}


M lib/msgstore.go => lib/msgstore.go +15 -1
@@ 67,7 67,6 @@ func (store *MessageStore) FetchHeaders(uids []uint32,
}

func (store *MessageStore) FetchBodies(uids []uint32, cb func(io.Reader)) {

	// 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 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(
	to *types.MessageInfo, from *types.MessageInfo) {


M widgets/aerc.go => widgets/aerc.go +0 -2
@@ 62,8 62,6 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
		tabs.Add(view, acct.Name)
	}

	tabs.Add(NewMessageViewer(), "[PATCH todo.sr.ht v2 …")

	return aerc
}


M widgets/msgviewer.go => widgets/msgviewer.go +54 -118
@@ 2,124 2,58 @@ package widgets

import (
	"bytes"
	"fmt"
	"io"
	"os/exec"

	"github.com/emersion/go-imap"
	"github.com/gdamore/tcell"
	"github.com/mattn/go-runewidth"

	"git.sr.ht/~sircmpwn/aerc2/lib"
	"git.sr.ht/~sircmpwn/aerc2/lib/ui"
	"git.sr.ht/~sircmpwn/aerc2/worker/types"
)

type MessageViewer struct {
	mail io.Reader
	pipe io.Writer
	grid *ui.Grid
	term *Terminal
}

var testMsg = `Makes the following changes to the Event type:

* make 'user' and 'ticket' nullable since some events require it
* add 'by_user' and 'from_ticket' to enable mentions
* 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
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()
}

 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(store *lib.MessageStore,
	msg *types.MessageInfo) *MessageViewer {

func NewMessageViewer() *MessageViewer {
	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},
	}).Columns([]ui.GridSpec{
		{ui.SIZE_WEIGHT, 1},
	})

	// TODO: let user specify additional headers to show by default
	headers := ui.NewGrid().Rows([]ui.GridSpec{
		{ui.SIZE_EXACT, 1},
		{ui.SIZE_EXACT, 1},
		{ui.SIZE_EXACT, 1},
		{ui.SIZE_EXACT, 1},
	}).Columns([]ui.GridSpec{
		{ui.SIZE_WEIGHT, 1},
		{ui.SIZE_WEIGHT, 1},


@@ 127,25 61,19 @@ func NewMessageViewer() *MessageViewer {
	headers.AddChild(
		&HeaderView{
			Name:  "From",
			Value: "Ivan Habunek <ivan@habunek.com>",
			Value: formatAddresses(msg.Envelope.From),
		}).At(0, 0)
	headers.AddChild(
		&HeaderView{
			Name:  "To",
			Value: "~sircmpwn/sr.ht-dev@lists.sr.ht",
			Value: formatAddresses(msg.Envelope.To),
		}).At(0, 1)
	headers.AddChild(
		&HeaderView{
			Name: "Subject",
			Value: "[PATCH todo.sr.ht v2 1/3 Alter Event fields " +
				"and migrate data]",
			Name:  "Subject",
			Value: msg.Envelope.Subject,
		}).At(1, 0).Span(1, 2)
	headers.AddChild(
		&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)
	headers.AddChild(ui.NewFill(' ')).At(2, 0).Span(1, 2)

	body := ui.NewGrid().Rows([]ui.GridSpec{
		{ui.SIZE_WEIGHT, 1},


@@ 154,25 82,30 @@ func NewMessageViewer() *MessageViewer {
		{ui.SIZE_EXACT, 20},
	})

	cmd := exec.Command("sh", "-c", "./contrib/hldiff.py | less -R")
	cmd := exec.Command("less")
	pipe, _ := cmd.StdinPipe()
	term, _ := NewTerminal(cmd)
	term.OnStart = func() {
	// TODO: configure multipart view. I left a spot for it in the grid
	body.AddChild(term).At(0, 0).Span(1, 2)

	grid.AddChild(headers).At(0, 0)
	grid.AddChild(body).At(1, 0)

	viewer := &MessageViewer{
		pipe: pipe,
		grid: grid,
		term: term,
	}

	store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) {
		viewer.mail = reader
		go func() {
			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(body).At(1, 0)
	return &MessageViewer{grid, term}
	return viewer
}

func (mv *MessageViewer) Draw(ctx *ui.Context) {


@@ 205,7 138,10 @@ type HeaderView struct {
}

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 (
		hstyle tcell.Style
		vstyle tcell.Style


@@ 219,8 155,8 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
		hstyle = tcell.StyleDefault.Bold(true)
	}
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
	ctx.Printf(0, 0, hstyle, hv.Name)
	ctx.Printf(size, 0, vstyle, " "+hv.Value)
	ctx.Printf(0, 0, hstyle, name)
	ctx.Printf(size, 0, vstyle, value)
}

func (hv *HeaderView) Invalidate() {

M worker/imap/fetch.go => worker/imap/fetch.go +26 -6
@@ 30,6 30,18 @@ func (imapw *IMAPWorker) handleFetchMessageBodies(
	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(
	msg types.WorkerMessage, uids *imap.SeqSet, items []imap.FetchItem) {



@@ 43,12 55,8 @@ func (imapw *IMAPWorker) handleFetchMessages(
			section := &imap.BodySectionName{}
			for _msg := range messages {
				imapw.seqMap[_msg.SeqNum-1] = _msg.Uid
				if reader := _msg.GetBody(section); reader != nil {
					imapw.worker.PostMessage(&types.MessageBody{
						Reader: reader,
						Uid:    _msg.Uid,
					}, nil)
				} else {
				switch msg.(type) {
				case *types.FetchMessageHeaders:
					imapw.worker.PostMessage(&types.MessageInfo{
						BodyStructure: _msg.BodyStructure,
						Envelope:      _msg.Envelope,


@@ 56,6 64,18 @@ func (imapw *IMAPWorker) handleFetchMessages(
						InternalDate:  _msg.InternalDate,
						Uid:           _msg.Uid,
					}, 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 {

M worker/imap/worker.go => worker/imap/worker.go +2 -0
@@ 160,6 160,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
		w.handleFetchMessageHeaders(msg)
	case *types.FetchMessageBodies:
		w.handleFetchMessageBodies(msg)
	case *types.FetchMessageBodyPart:
		w.handleFetchMessageBodyPart(msg)
	case *types.DeleteMessages:
		w.handleDeleteMessages(msg)
	default: