~f4814n/frost

54bdde186ec68a150e27bfad73415e97bda5716e — Fabian Geiselhart 11 months ago 4b6b601
Rework event passing architecture and improve logging
9 files changed, 418 insertions(+), 261 deletions(-)

A cmd/frost/debug.go
R cmd/frost/{list_page.go => list_view.go}
R cmd/frost/{login_page.go => login_view.go}
M cmd/frost/main.go
A cmd/frost/merge_view.go
D cmd/frost/overview_page.go
M cmd/frost/platform_js.go
R cmd/frost/{room_page.go => room_view.go}
M cmd/frost/util.go
A cmd/frost/debug.go => cmd/frost/debug.go +12 -0
@@ 0,0 1,12 @@
// +build debug

package main

import (
	_ "net/http/pprof"
	"net/http"
)

func init() {
	http.ListenAndServe(":6060", nil)
}

R cmd/frost/list_page.go => cmd/frost/list_view.go +25 -14
@@ 5,7 5,7 @@ import (
	"image"
	"unicode"

	log "github.com/sirupsen/logrus"
	"github.com/sirupsen/logrus"
	"golang.org/x/exp/shiny/materialdesign/icons"

	"gioui.org/gesture"


@@ 18,7 18,7 @@ import (
	"git.sr.ht/~f4814n/frost/matrix"
)

type ListPage struct {
type ListView struct {
	rx, tx chan Event

	matrixEvents chan matrix.Event


@@ 27,24 27,29 @@ type ListPage struct {
	roomList *RoomList

	menuButton *widget.Clickable

	log *logrus.Entry
}

func NewListPage(cli *matrix.Client) *ListPage {
	return &ListPage{
func NewListView(cli *matrix.Client) *ListView {
	log := logrus.WithField("component", "list_view")
	return &ListView{
		cli:          cli,
		matrixEvents: make(chan matrix.Event, 100),
		roomList:     NewRoomList(),
		roomList:     NewRoomList(log),
		menuButton:   new(widget.Clickable),
		log: log,
	}
}

func (l *ListPage) Start(rx, tx chan Event) {
	l.rx, l.tx = rx, tx
func (l *ListView) Start(rx, tx chan Event) {
	logStart(l.log)

	l.rx, l.tx = rx, tx
	l.cli.Notify(l.matrixEvents)
}

func (l *ListPage) Layout(gtx Gtx) Dims {
func (l *ListView) Layout(gtx Gtx) Dims {
	l.update(gtx)

	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,


@@ 53,7 58,7 @@ func (l *ListPage) Layout(gtx Gtx) Dims {
	)
}

func (l *ListPage) topbar(gtx Gtx) Dims {
func (l *ListView) topbar(gtx Gtx) Dims {
	return layout.Stack{Alignment: layout.SW}.Layout(gtx,
		layout.Expanded(fill{theme.Color.Primary}.Layout),
		layout.Stacked(func(gtx Gtx) Dims {


@@ 70,11 75,14 @@ func (l *ListPage) topbar(gtx Gtx) Dims {
	)
}

func (l *ListPage) update(gtx Gtx) {
func (l *ListView) update(gtx Gtx) {
	for _, room := range l.roomList.rooms {
		for _, e := range room.click.Events(gtx) {
			if e.Type == gesture.TypeClick {
				l.tx <- ViewRoomEvent{Room: room.room}
				l.tx <- LoadViewEvent{
					Position: Middle,
					View: NewRoomView(l.cli, room.room),
				}
			}
		}
	}


@@ 82,7 90,7 @@ func (l *ListPage) update(gtx Gtx) {
	for {
		select {
		case event := <-l.rx:
			log.WithField("page", "list").Warnf("%+v", event)
			logEvent(l.log, event)
		case event := <-l.matrixEvents:
			l.roomList.NewEvent(event)
		default:


@@ 91,17 99,19 @@ func (l *ListPage) update(gtx Gtx) {
	}
}

func (l *ListPage) Stop() {
func (l *ListView) Stop() {
}

// RoomList is a widget that displays a list of rooms
type RoomList struct {
	list  *layout.List
	rooms []roomListElement
	log *logrus.Entry
}

func NewRoomList() *RoomList {
func NewRoomList(log *logrus.Entry) *RoomList {
	return &RoomList{
		log: log,
		list: &layout.List{
			Axis: layout.Vertical,
		},


@@ 123,6 133,7 @@ func (w *RoomList) NewEvent(event matrix.Event) {

	for i, elem := range w.rooms {
		if elem.room == room {
			w.log.WithField("room", room.Displayname()).Debug("Adding room")
			w.rooms = append(w.rooms[:i], w.rooms[i+1:]...)
			break
		}

R cmd/frost/login_page.go => cmd/frost/login_view.go +15 -10
@@ 7,10 7,10 @@ import (
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	log "github.com/sirupsen/logrus"
	"github.com/sirupsen/logrus"
)

type LoginPage struct {
type LoginView struct {
	rx, tx chan Event

	usernameEditor *widget.Editor


@@ 20,6 20,8 @@ type LoginPage struct {
	errorMessage string

	loginInProcess bool

	log *logrus.Entry
}

type LoginStartEvent struct {


@@ 30,20 32,23 @@ type LoginErrorEvent struct {
	Err error
}

func NewLoginPage() *LoginPage {
	return &LoginPage{
func NewLoginView() *LoginView {
	return &LoginView{
		loginInProcess: false,
		usernameEditor: &widget.Editor{Submit: true, SingleLine: true},
		passwordEditor: &widget.Editor{Submit: true, SingleLine: true, Mask: '•'},
		loginButton:    new(widget.Clickable),
		log: logrus.WithField("component", "login_view"),
	}
}

func (l *LoginPage) Start(rx, tx chan Event) {
func (l *LoginView) Start(rx, tx chan Event) {
	logStart(l.log)

	l.rx, l.tx = rx, tx
}

func (l *LoginPage) update() {
func (l *LoginView) update() {
	if l.loginButton.Clicked() {
		l.loginInProcess = true



@@ 56,12 61,12 @@ func (l *LoginPage) update() {
	for {
		select {
		case e := <-l.rx:
			logEvent(l.log, e)

			switch e := e.(type) {
			case LoginErrorEvent:
				l.loginInProcess = false
				l.errorMessage = e.Err.Error()
			default:
				log.Warnf("%#v", e)
			}
		default:
			return


@@ 69,7 74,7 @@ func (l *LoginPage) update() {
	}
}

func (l *LoginPage) Layout(gtx Gtx) Dims {
func (l *LoginView) Layout(gtx Gtx) Dims {
	l.update()

	if l.loginInProcess {


@@ 100,5 105,5 @@ func (l *LoginPage) Layout(gtx Gtx) Dims {
	})
}

func (l *LoginPage) Stop() {
func (l *LoginView) Stop() {
}

M cmd/frost/main.go => cmd/frost/main.go +89 -46
@@ 1,7 1,7 @@
package main

import (
	"fmt"
	"context"
	"net/http"

	"gioui.org/app"


@@ 12,7 12,7 @@ import (
	"gioui.org/unit"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost/matrix"
	log "github.com/sirupsen/logrus"
	"github.com/sirupsen/logrus"
)

var theme *material.Theme


@@ 21,7 21,7 @@ type Gtx = layout.Context
type Dims = layout.Dimensions
type Event interface{}

type Page interface {
type View interface {
	Start(rx chan Event, tx chan Event)
	Layout(Gtx) Dims
	Stop()


@@ 40,9 40,10 @@ type App struct {
	window *app.Window
	cli *matrix.Client
	rx, tx chan Event
	page   Page
	view   View
	config AppConfig
	platform Platform
	log *logrus.Entry
}

func newApp() *App {


@@ 59,92 60,134 @@ func newApp() *App {
			app.Title("Frost"),
		),
		platform: platform,
		log: logrus.WithField("component", "app"),
	}
}

func (a *App) run() error {
	a.view = NewMergeView(a.cli)
	theme = material.NewTheme(gofont.Collection())

	for {
		select {
		case e := <-a.window.Events():
			switch e := e.(type) {
			case system.StageEvent:
				theme = material.NewTheme(gofont.Collection())
				var err error
				a.config, err = a.platform.LoadConfig()
				if err != nil {
					log.WithError(err).Warn("platform: Failed to load config")
					a.log.WithError(err).Warn("platform: Failed to load config")
				}
				page := a.initialPage()
				a.page = page
				page.Start(a.tx, a.rx)

				a.view.Start(a.tx, a.rx)
				a.initialView()
			case system.FrameEvent:
				var ops op.Ops
				gtx := layout.NewContext(&ops, e)
				a.Layout(gtx)
				a.view.Layout(gtx)
				e.Frame(gtx.Ops)
			case system.DestroyEvent:
				return e.Err
			default:
				log.Tracef("%#v", e)
			}
			// case app.pageEvents:

		case e := <-a.rx:
			logEvent(a.log, e)

			switch e := e.(type) {
			case LoginStartEvent:
				go func() {
					log.Info("Logging in")
					err := a.cli.Login(e.Username, e.Password)
					if err != nil {
						log.WithError(err).Info("Failed to log in")
						a.tx <- LoginErrorEvent{Err: err}
						return
					}
					log.Info("Successfully authenticated")

					a.config.Session = &SessionConfig{
						MxID: a.cli.MxID(),
						DeviceID: a.cli.DeviceID(),
					}
					err = a.platform.FlushConfig(a.config)
					if err != nil {
						log.WithError(err).Warn("platform: Failed to flush config")
					}

					a.page.Stop()
					a.page = NewOverviewPage(a.cli)
					a.page.Start(a.tx, a.rx)
				}()
				go a.login(e)
			case InvalidationEvent:
				a.window.Invalidate()
			default:
				log.WithField("event", fmt.Sprintf("%#v", e)).Warn("Unhandled event")
			}
		}
	}
}

func (a *App) initialPage() Page {
func (a *App) login(event LoginStartEvent) {
	err := a.cli.Login(event.Username, event.Password)
	if err != nil {
		a.log.WithError(err).Debug("Failed to log in")
		a.tx <- LoginErrorEvent{Err: err}
		return
	}
	a.log.Debug("Successfully authenticated")

	a.config.Session = &SessionConfig{
		MxID: a.cli.MxID(),
		DeviceID: a.cli.DeviceID(),
	}
	err = a.platform.FlushConfig(a.config)
	if err != nil {
		a.log.WithError(err).Warn("platform: Failed to flush config")
	}

	a.tx <- UnloadViewEvent{Overlay}
	a.tx <- LoadViewEvent{
		Position: Left,
		View: NewListView(a.cli),
	}

	matrixEvents := make(chan matrix.Event, 1000)
	a.cli.Notify(matrixEvents)

	a.sync()
}

func (a *App) initialView() {
	if a.config.Session != nil {
		err := a.cli.LoadToken(a.config.Session.MxID, a.config.Session.DeviceID)
		if err != nil {
			log.WithField("error", err).Error("Failed to reuse session")
			a.log.WithField("error", err).Error("Failed to reuse session")
		}
		a.sync()

		a.tx <- LoadViewEvent{
			Position: Left,
			View: NewListView(a.cli),
		}
	} else {
		a.tx <- LoadViewEvent{
			Position: Overlay,
			View: NewLoginView(),
		}
		return NewOverviewPage(a.cli)
	}

	return NewLoginPage()
	a.rx <- InvalidationEvent{}
}

func (a *App) Layout(gtx Gtx) {
	a.page.Layout(gtx)
func (a *App) sync() {
	// TODO Gracefully stop this goroutine
	go a.cli.Sync(context.TODO(), &matrix.SyncOpts{
		OnError: func(err error) error {
			logrus.WithFields(logrus.Fields{
				"component": "matrix",
				"error": err,
			}).Warn("Sync error")
			return nil
		},
		Timeout: 10000,
	})

	// TODO Gracefully stop this goroutine
	go func() {
		c := make(chan matrix.Event, 100)
		a.cli.Notify(c)
		for event := range c {
			logrus.WithFields(logrus.Fields{
				"component": "matrix",
				"event": event,
			}).Trace("Received event")
			a.rx <- InvalidationEvent{}
		}
	}()
}

func main() {
	log.SetLevel(log.DebugLevel)
	logrus.SetLevel(logrus.TraceLevel)
	go func() {
		a := newApp()
		if err := a.run(); err != nil {
			log.Fatal(err)
			logrus.Fatal(err)
		}
	}()
	app.Main()

A cmd/frost/merge_view.go => cmd/frost/merge_view.go +233 -0
@@ 0,0 1,233 @@
package main

import (
	"image"

	"gioui.org/layout"
	"gioui.org/unit"
	"git.sr.ht/~f4814n/frost/matrix"
	"github.com/sirupsen/logrus"
)

type InvalidationEvent struct{}

type BackEvent struct{}

type Position int

const (
	Left Position = iota
	Middle
	Right
	Overlay
)

type LoadViewEvent struct {
	Position Position
	View     View
}

type UnloadViewEvent struct {
	Position Position
}

// MergeView implements a responsive three grid view.
type MergeView struct {
	rx, tx             chan Event
	rxL, rxM, rxR, rxO chan Event
	txL, txM, txR, txO chan Event
	allRX, allTX       chan Event

	stop chan struct{}

	log *logrus.Entry

	cli *matrix.Client

	left, right, middle, overlay View
}

func NewMergeView(cli *matrix.Client) *MergeView {
	return &MergeView{
		cli: cli,

		log: logrus.WithField("component", "merge_view"),

		txL: make(chan Event, 100),
		txM: make(chan Event, 100),
		txR: make(chan Event, 100),
		txO: make(chan Event, 100),

		rxL: make(chan Event, 100),
		rxM: make(chan Event, 100),
		rxR: make(chan Event, 100),
		rxO: make(chan Event, 100),

		allRX: make(chan Event, 100),
		allTX: make(chan Event, 100),
	}
}

func (o *MergeView) Start(rx, tx chan Event) {
	logStart(o.log)

	o.rx, o.tx = rx, tx
}

// focused returns the rightmost View
func (o *MergeView) focused() View {
	if o.right != nil {
		return o.right
	} else if o.middle != nil {
		return o.middle
	} else if o.left != nil {
		return o.left
	}

	return nil
}

func (o *MergeView) Layout(gtx Gtx) Dims {
	o.update(gtx)

	leftsize := gtx.Px(unit.Dp(400))
	rightsize := gtx.Px(unit.Dp(400))

	if o.overlay != nil {
		return o.overlay.Layout(gtx)
	}

	if o.left == nil && o.middle == nil && o.right == nil {
		return NoView{}.Layout(gtx)
	}

	if gtx.Constraints.Max.X < gtx.Px(unit.Dp(720)) {
		if view := o.focused(); view != nil {
			return view.Layout(gtx)
		}
		return NoView{}.Layout(gtx)
	} else if gtx.Constraints.Max.X < gtx.Px(unit.Dp(1100)) {
		return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
			layout.Rigid(func(gtx Gtx) Dims {
				gtx.Constraints.Max.X = leftsize
				return o.left.Layout(gtx)
			}),
			layout.Flexed(1, func(gtx Gtx) Dims {
				if view := o.focused(); view != nil && view != o.left {
					return view.Layout(gtx)
				} else {
					return NoView{}.Layout(gtx)
				}
			}),
		)
	} else {
		return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
			layout.Rigid(func(gtx Gtx) Dims {
				gtx.Constraints.Max.X = leftsize
				return o.left.Layout(gtx)
			}),
			layout.Flexed(1, func(gtx Gtx) Dims {
				if o.middle != nil {
					return o.middle.Layout(gtx)
				}
				return NoView{}.Layout(gtx)
			}),
			layout.Rigid(func(gtx Gtx) Dims {
				if o.right != nil {
					gtx.Constraints.Max.X = rightsize
					return o.right.Layout(gtx)
				}
				return NoView{}.Layout(gtx)
			}),
		)
	}
}

func (o *MergeView) update(gtx Gtx) {
	for {
		select {
		case e := <- o.rxL:
			o.receiveEvent(e)
		case e := <- o.rxM:
			o.receiveEvent(e)
		case e := <- o.rxR:
			o.receiveEvent(e)
		case e := <- o.rxO:
			o.receiveEvent(e)
		case e := <- o.rx:
			o.receiveEvent(e)
		default:
			return
		}
	}
}

func (o *MergeView) receiveEvent(event interface{}) {
	logEvent(o.log, event)

	switch event := event.(type) {
	case LoadViewEvent:
		view := event.View

		switch event.Position {
		case Left:
			view.Start(o.txL, o.rxL)
			o.left = view
		case Right:
			view.Start(o.txR, o.rxR)
			o.right = view
		case Middle:
			view.Start(o.txM, o.rxM)
			o.middle = view
		case Overlay:
			view.Start(o.txO, o.rxO)
			o.overlay = view
		}
	case UnloadViewEvent:
		switch event.Position {
		case Left:
			o.left.Stop()
			o.left = nil
		case Right:
			o.right.Stop()
			o.right = nil
		case Middle:
			o.middle.Stop()
			o.middle = nil
		case Overlay:
			o.overlay.Stop()
			o.overlay = nil
		}
	case BackEvent:
		o.focused().Stop()
		if o.focused() == o.right {
			o.right = nil
		} else {
			o.middle = nil
		}
	default:
		o.txL <- event
		o.txM <- event
		o.txR <- event
		o.txO <- event
		o.tx <- event
	}
}

func (o *MergeView) Stop() {
}

type NoView struct{}

func (v NoView) Layout(gtx Gtx) Dims {
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx Gtx) Dims {
			return layout.Stack{Alignment: layout.SW}.Layout(gtx,
				layout.Expanded(fill{theme.Color.Primary}.Layout),
				layout.Stacked(func(gtx Gtx) Dims {
					return Dims{Size: image.Point{X: gtx.Constraints.Max.X, Y: gtx.Px(unit.Dp(48))}}
				}),
			)
		}),
	)
}

D cmd/frost/overview_page.go => cmd/frost/overview_page.go +0 -164
@@ 1,164 0,0 @@
package main

import (
	"context"
	"image"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/unit"
	"git.sr.ht/~f4814n/frost/matrix"
	log "github.com/sirupsen/logrus"
)

type InvalidationEvent struct{}

type BackEvent struct{}

type ViewRoomEvent struct {
	Room matrix.Room
}

type OverviewPage struct {
	rx, tx         chan Event
	listRx, listTx chan Event
	roomRx, roomTx chan Event

	cli    *matrix.Client
	events chan matrix.Event

	roomPage *RoomPage
	listPage *ListPage
}

func NewOverviewPage(cli *matrix.Client) *OverviewPage {
	return &OverviewPage{
		cli:      cli,
		events:   make(chan matrix.Event, 100),
		listPage: NewListPage(cli),
		listRx:   make(chan Event, 100),
		listTx:   make(chan Event, 100),
		roomRx:   make(chan Event, 100),
		roomTx:   make(chan Event, 100),
	}
}

func (o *OverviewPage) Start(rx, tx chan Event) {
	o.rx, o.tx = rx, tx

	o.listPage.Start(o.listTx, o.listRx)

	o.cli.Notify(o.events)

	go o.cli.Sync(context.TODO(), &matrix.SyncOpts{
		OnError: func(err error) error {
			log.WithField("error", err).Warn("Sync error")
			return nil
		},
		Timeout: 10000,
	})

	go func() {
		c := make(chan matrix.Event, 100)
		o.cli.Notify(c)
		for range c {
			o.tx <- InvalidationEvent{}
		}
	}()
}

func (o *OverviewPage) Layout(gtx Gtx) Dims {
	o.update(gtx)

	if gtx.Constraints.Max.X < gtx.Px(unit.Dp(720)) {
		return o.compactLayout(gtx)
	}

	return o.fullLayout(gtx)

}

func (o *OverviewPage) compactLayout(gtx Gtx) Dims {
	if o.roomPage != nil {
		return o.roomPage.Layout(gtx)
	}

	return o.listPage.Layout(gtx)
}

func (o *OverviewPage) fullLayout(gtx Gtx) Dims {
	leftsize := gtx.Px(unit.Dp(400))
	rightsize := gtx.Constraints.Min.X - leftsize

	{
		stack := op.Push(gtx.Ops)
		gtx := gtx
		gtx.Constraints = layout.Exact(image.Pt(leftsize, gtx.Constraints.Max.Y))
		o.listPage.Layout(gtx)
		stack.Pop()
	}

	{
		stack := op.Push(gtx.Ops)
		gtx := gtx
		gtx.Constraints = layout.Exact(image.Pt(rightsize, gtx.Constraints.Max.Y))
		op.Offset(f32.Pt(float32(leftsize), 0)).Add(gtx.Ops)

		if o.roomPage != nil {
			o.roomPage.Layout(gtx)
		} else {
			NoRoomPage{}.Layout(gtx)
		}

		stack.Pop()
	}

	return layout.Dimensions{Size: gtx.Constraints.Max}
}

func (o *OverviewPage) update(gtx Gtx) {
	for {
		select {
		case event := <-o.rx:
			log.Warnf("%+v", event)
		case event := <-o.listRx:
			switch event := event.(type) {
			case ViewRoomEvent:
				log.WithField("room", event.Room.ID).Info("Selected room")
				o.roomPage = NewRoomPage(o.cli, event.Room)
				o.roomPage.Start(o.roomTx, o.roomRx)
			default:
				log.Warnf("%+v", event)
			}
		case event := <-o.roomRx:
			switch event.(type) {
			case BackEvent:
				o.roomPage.Stop()
				o.roomPage = nil
			default:
				log.Warnf("%+v", event)
			}
		default:
			return
		}
	}
}

func (o *OverviewPage) Stop() {
}

type NoRoomPage struct{}

func (p NoRoomPage) Layout(gtx Gtx) Dims {
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx Gtx) Dims {
			return layout.Stack{Alignment: layout.SW}.Layout(gtx,
				layout.Expanded(fill{theme.Color.Primary}.Layout),
				layout.Stacked(func(gtx Gtx) Dims {
					return Dims{Size: image.Point{X: gtx.Constraints.Max.X, Y: gtx.Px(unit.Dp(48))}}
				}),
			)
		}),
	)
}

M cmd/frost/platform_js.go => cmd/frost/platform_js.go +6 -3
@@ 2,19 2,22 @@ package main

import (
	"git.sr.ht/~f4814n/frost/matrix"
	log "github.com/sirupsen/logrus"
	"github.com/sirupsen/logrus"
	backend "git.sr.ht/~f4814n/frost/matrix/backend/memory"
)

type Platform struct {
	log *logrus.Entry
}

func InitPlatform() Platform {
	return Platform{}
	return Platform{
		log: logrus.WithField("component", "platform_js"),
	}
}

func (p Platform) LoadConfig() (cfg AppConfig, err error) {
	log.Warn("This platform does not support persisting information")
	p.log.Warn("This platform does not support persisting information")
	return
}


R cmd/frost/room_page.go => cmd/frost/room_view.go +27 -24
@@ 4,7 4,7 @@ import (
	"fmt"
	"time"

	log "github.com/sirupsen/logrus"
	"github.com/sirupsen/logrus"
	"golang.org/x/exp/shiny/materialdesign/icons"

	"gioui.org/layout"


@@ 18,7 18,7 @@ type SentRoomEvent struct {
	Err error
}

type RoomPage struct {
type RoomView struct {
	rx, tx chan Event

	matrixEvents chan matrix.Event


@@ 33,10 33,12 @@ type RoomPage struct {
	settingsButton  *widget.Clickable

	messageComposer *messageComposer

	log *logrus.Entry
}

func NewRoomPage(cli *matrix.Client, room matrix.Room) *RoomPage {
	return &RoomPage{
func NewRoomView(cli *matrix.Client, room matrix.Room) *RoomView {
	return &RoomView{
		cli:             cli,
		matrixEvents:    make(chan matrix.Event, 100),
		pastEvents:      make(chan matrix.Event, 100),


@@ 46,28 48,21 @@ func NewRoomPage(cli *matrix.Client, room matrix.Room) *RoomPage {
		backButton:      new(widget.Clickable),
		settingsButton:      new(widget.Clickable),
		messageComposer: newMessageComposer(),
		log: logrus.WithField("component", "room_view"),
	}
}

func (r *RoomPage) Start(rx, tx chan Event) {
func (r *RoomView) Start(rx, tx chan Event) {
	r.rx, r.tx = rx, tx

	r.room.Notify(r.matrixEvents)

	c := make(chan matrix.Event)
	go func() {
		for e := range c {
			log.Debugf("%#v", e)
		}
	}()
	r.room.Notify(c)

	// TODO Only load history which is actually needed
	// TODO Stop this goroutine
	go func() {
		for r.history.Next() {
			if r.history.Err != nil {
				log.WithField("error", r.history.Err).Warn("Error while traversing history")
				r.log.WithField("error", r.history.Err).Warn("Error while traversing history")
				continue
			}



@@ 78,13 73,13 @@ func (r *RoomPage) Start(rx, tx chan Event) {
	}()
}

func (r *RoomPage) update() {
func (r *RoomView) update() {
	if r.backButton.Clicked() {
		r.tx <- BackEvent{}
	}

	if r.settingsButton.Clicked() {
		log.Warn("Not implemented")
		r.log.Warn("Not implemented")
	}

	if r.messageComposer.Submitted() {


@@ 102,11 97,13 @@ func (r *RoomPage) update() {
	for {
		select {
		case event := <-r.rx:
			r.log.WithField("event", event).Trace("Received event")

			switch event := event.(type) {
			case SentRoomEvent:
				r.messageComposer.Reenable(event.Err)
			default:
				log.WithField("event", event).Warn("Unexpected event")
				r.log.WithField("event", event).Warn("Unexpected event")
			}
		case event := <-r.matrixEvents:
			switch event.(type) {


@@ 123,10 120,10 @@ func (r *RoomPage) update() {
	}
}

func (r *RoomPage) Stop() {
func (r *RoomView) Stop() {
}

func (r *RoomPage) Layout(gtx Gtx) Dims {
func (r *RoomView) Layout(gtx Gtx) Dims {
	r.update()

	return layout.Flex{Alignment: layout.Start, Axis: layout.Vertical}.Layout(gtx,


@@ 136,7 133,7 @@ func (r *RoomPage) Layout(gtx Gtx) Dims {
	)
}

func (r *RoomPage) topbar(gtx Gtx) Dims {
func (r *RoomView) topbar(gtx Gtx) Dims {
	return layout.Stack{Alignment: layout.SW}.Layout(gtx,
		layout.Expanded(fill{theme.Color.Primary}.Layout),
		layout.Stacked(func(gtx Gtx) Dims {


@@ 200,13 197,19 @@ func (w *roomHistory) element(gtx Gtx, index int) Dims {
	if ownEvent(w.user, msg) {
		in.Left, in.Right = in.Right, in.Left
	}
	return in.Layout(gtx, event{Event: msg, Own: ownEvent(w.user, msg)}.Layout)
	return in.Layout(gtx, event{
		Event: msg,
		Own: ownEvent(w.user, msg),
		log: logrus.WithField("event", msg),
	}.Layout)
}

// event is a widget that displays a single event in the room history
type event struct {
	Event matrix.Event
	Own   bool

	log *logrus.Entry
}

func (e event) Layout(gtx Gtx) Dims {


@@ 267,7 270,7 @@ func (e event) layoutStateEventContent(gtx Gtx) Dims {
			str = fmt.Sprintf("%s changed the room topic to \"%s\"", senderName, topic)
		} else {
			str = fmt.Sprintf("%s removed the room topic", senderName)
			log.WithField("event", ev).Debug("Invalid event")
			e.log.Debug("Invalid event")
		}
		lbl := material.Body1(theme, str)
		return lbl.Layout(gtx)


@@ 280,7 283,7 @@ func (e event) layoutStateEventContent(gtx Gtx) Dims {
				str = fmt.Sprintf("%s disallowed guests to join", senderName)
			}
		} else {
			log.WithField("event", ev).Debug("Invalid event")
			e.log.Debug("Invalid event")
			str = "Invalid"
		}
		return material.Body1(theme, str).Layout(gtx)


@@ 290,7 293,7 @@ func (e event) layoutStateEventContent(gtx Gtx) Dims {
		if access, ok := ev.Content["history_visibility"]; ok {
			str = fmt.Sprintf("%s set the history visibility to \"%s\"", senderName, access)
		} else {
			log.WithField("event", ev).Debug("Invalid event")
			e.log.Debug("Invalid event")
			str = "Invalid"
		}
		return material.Body1(theme, str).Layout(gtx)

M cmd/frost/util.go => cmd/frost/util.go +11 -0
@@ 3,6 3,7 @@ package main
import (
	"image/color"
	"time"
	"fmt"

	"gioui.org/f32"
	"gioui.org/layout"


@@ 12,6 13,7 @@ import (
	"gioui.org/unit"
	"gioui.org/widget"
	"git.sr.ht/~f4814n/frost/matrix"
	"github.com/sirupsen/logrus"
)

func rgb(c uint32) color.RGBA {


@@ 123,3 125,12 @@ func mustIcon(data []byte) *widget.Icon {
	}
	return ico
}


func logEvent(log *logrus.Entry, event interface{}) {
	log.WithField("event", fmt.Sprintf("%#v", event)).Trace("Received event")
}

func logStart(log *logrus.Entry) {
	log.Debug("Starting view")
}