~f4814n/frost

47e7e61aa78e553175eb3c244f1489ebe4e9c9f2 — Fabian Geiselhart 1 year, 3 months ago eeccf42
Split RoomList and RoomHistory into RoomPage and ListPage
5 files changed, 414 insertions(+), 302 deletions(-)

A list_page.go
M login_page.go
M main.go
M overview_page.go
A room_page.go
A list_page.go => list_page.go +214 -0
@@ 0,0 1,214 @@
package main

import (
	"fmt"
	"image"
	"unicode"

	log "github.com/sirupsen/logrus"

	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/matrix"
)

type ListPage struct {
	rx, tx chan Event

	matrixEvents chan matrix.Event

	cli      *matrix.Client
	roomList *RoomList
}

func NewListPage(cli *matrix.Client) *ListPage {
	return &ListPage{
		cli:          cli,
		matrixEvents: make(chan matrix.Event, 100),
		roomList:     NewRoomList(),
	}
}

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

	l.cli.Notify(l.matrixEvents)
}

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

	return l.roomList.Layout(gtx)
}

func (l *ListPage) 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}
			}
		}
	}

	for {
		select {
		case event := <-l.rx:
			log.WithField("page", "list").Warnf("%+v", event)
		case event := <-l.matrixEvents:
			l.roomList.NewEvent(event)
		default:
			return
		}
	}
}

func (l *ListPage) Stop() {
}

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

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

// NewEvent updates the room list when a new event arrives
func (w *RoomList) NewEvent(event matrix.Event) {
	var room matrix.Room

	switch event := event.(type) {
	case matrix.RoomEvent:
		room = event.Room
	case matrix.StateEvent:
		room = event.Room
	default:
		return
	}

	for i, elem := range w.rooms {
		if elem.room == room {
			w.rooms = append(w.rooms[:i], w.rooms[i+1:]...)
			break
		}
	}
	w.rooms = append([]roomListElement{NewRoomListElement(room, event)}, w.rooms...)
}

// Layout implements the layout.Widget interface
func (w *RoomList) Layout(gtx Gtx) Dims {
	return w.list.Layout(gtx, len(w.rooms), func(gtx Gtx, index int) Dims {
		return w.rooms[index].Layout(gtx)
	})
}

// roomListElement is a element in the room list
type roomListElement struct {
	room      matrix.Room
	lastEvent matrix.Event
	click     *gesture.Click
}

func NewRoomListElement(room matrix.Room, event matrix.Event) roomListElement {
	return roomListElement{
		room:      room,
		lastEvent: event,
		click:     new(gesture.Click),
	}
}

func (w *roomListElement) Layout(gtx Gtx) Dims {
	in := layout.Inset{
		Left:  unit.Dp(16),
		Right: unit.Dp(16),
	}

	return in.Layout(gtx, func(gtx Gtx) Dims {
		in := layout.Inset{Top: unit.Dp(16), Bottom: unit.Dp(16)}
		dims := in.Layout(gtx, func(gtx Gtx) Dims {
			return centerRowOpts().Layout(gtx,
				layout.Rigid(w.layoutIcon),
				layout.Rigid(func(gtx Gtx) Dims {
					return column().Layout(gtx,
						layout.Rigid(w.layoutUpper),
						layout.Rigid(w.layoutLastEvent),
					)
				}),
			)
		})
		pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
		w.click.Add(gtx.Ops)
		return dims
	})
}

func (w *roomListElement) layoutIcon(gtx Gtx) Dims {
	in := layout.Inset{Right: unit.Dp(12)}

	var initial string
	for _, c := range w.room.Displayname() {
		initial = string(unicode.ToUpper(c))
		break
	}

	return in.Layout(gtx, InitialSign{Initial: initial}.Layout)
}

func (w *roomListElement) layoutUpper(gtx Gtx) Dims {
	bgtextcol := rgb(0xbbbbbb)
	return baseline().Layout(gtx,
		layout.Rigid(func(gtx Gtx) Dims {
			lbl := material.H6(theme, w.room.Displayname())
			lbl.MaxLines = 1
			return lbl.Layout(gtx)
		}),
		layout.Flexed(1, func(gtx Gtx) Dims {
			gtx.Constraints.Min.X = gtx.Constraints.Max.X
			in := layout.Inset{Left: unit.Dp(2)}
			return in.Layout(gtx, func(gtx Gtx) Dims {
				lbl := material.Caption(theme, formatTime(eventTime(w.lastEvent)))
				lbl.MaxLines = 1
				lbl.Color = bgtextcol
				lbl.Alignment = text.End
				return lbl.Layout(gtx)
			})
		}),
	)
}

func (w *roomListElement) layoutLastEvent(gtx Gtx) Dims {
	bgtextcol := rgb(0xbbbbbb)
	in := layout.Inset{Top: unit.Dp(6)}
	return in.Layout(gtx, func(gtx Gtx) Dims {
		lbl := material.Body2(theme, formatEvent(w.lastEvent))
		lbl.Color = bgtextcol
		lbl.MaxLines = 1
		return lbl.Layout(gtx)
	})
}

func formatEvent(event matrix.Event) string {
	switch event := event.(type) {
	case matrix.RoomEvent:
		switch event.Type {
		case "m.room.message":
			if event.Content["msgtype"] == "m.text" {
				return fmt.Sprintf("%s: %s", event.Sender.Displayname(), event.Content["body"])
			}
			return fmt.Sprintf("Unsupported message type: %s", event.Content["msgtype"])
		}
	case matrix.StateEvent:
	}

	return fmt.Sprintf("%#v", event)
}

M login_page.go => login_page.go +2 -2
@@ 70,14 70,14 @@ func (l *LoginPage) update() {
	}
}

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

	if l.loginInProcess {
		gtx = gtx.Disabled()
	}

	center(gtx, image.Point{X: gtx.Px(unit.Dp(280)), Y: gtx.Px(unit.Dp(480))}, func(gtx Gtx) Dims {
	return center(gtx, image.Point{X: gtx.Px(unit.Dp(280)), Y: gtx.Px(unit.Dp(480))}, func(gtx Gtx) Dims {
		gtx.Constraints.Min = gtx.Constraints.Max
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
			layout.Rigid(material.Editor(theme, l.usernameEditor, "username").Layout),

M main.go => main.go +1 -1
@@ 23,7 23,7 @@ type Event interface{}

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


M overview_page.go => overview_page.go +23 -299
@@ 2,17 2,8 @@ package main

import (
	"context"
	"fmt"
	"image"
	"time"
	"unicode"

	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/matrix"
	log "github.com/sirupsen/logrus"
)


@@ 26,27 17,28 @@ type ViewRoomEvent struct {
type OverviewPage struct {
	rx, tx chan Event

	cli        *matrix.Client
	events     chan matrix.Event
	roomEvents chan matrix.Event
	cli    *matrix.Client
	events chan matrix.Event

	roomList    *RoomList
	roomHistory *RoomHistory
	roomPage *RoomPage
	listPage *ListPage
}

func NewOverviewPage(cli *matrix.Client) *OverviewPage {
	return &OverviewPage{
		cli:         cli,
		events:      make(chan matrix.Event, 100),
		roomEvents:  make(chan matrix.Event, 100),
		roomList:    NewRoomList(),
		roomHistory: new(RoomHistory),
		cli:      cli,
		events:   make(chan matrix.Event, 100),
		listPage: NewListPage(cli),
		roomPage: new(RoomPage),
	}
}

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

	o.roomPage.Start(o.tx, o.rx)
	o.listPage.Start(o.tx, o.rx)

	o.cli.Notify(o.events)

	go o.cli.Sync(context.TODO(), &matrix.SyncOpts{


@@ 66,37 58,27 @@ func (o *OverviewPage) Start(rx, tx chan Event) {
	}()
}

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

	layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
		layout.Flexed(1, o.roomList.Layout),
		layout.Flexed(3, o.roomHistory.Layout),
	return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
		layout.Flexed(1, o.listPage.Layout),
		layout.Flexed(3, o.roomPage.Layout),
	)
}

func (o *OverviewPage) update(gtx Gtx) {
	for _, room := range o.roomList.rooms {
		for _, e := range room.click.Events(gtx) {
			if e.Type == gesture.TypeClick {
				log.Infof("selected room: %s", room.room.ID)
				if o.roomHistory.list != nil {
					o.roomHistory.room.Stop(o.roomEvents)
				}
				o.roomHistory = NewRoomHistory(o.cli, room.room)
				room.room.Notify(o.roomEvents)
			}
		}
	}

	for {
		select {
		case event := <-o.events:
			o.roomList.NewEvent(event)
		case event := <-o.roomEvents:
			o.roomHistory.AddEvent(event)
		case event := <-o.rx:
			log.Warnf("%#v", event)
			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.tx, o.rx)
			default:
				log.WithField("page", "overview").Warnf("%+v", event)
			}
		default:
			return
		}


@@ 105,261 87,3 @@ func (o *OverviewPage) update(gtx Gtx) {

func (o *OverviewPage) Stop() {
}

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

// NewEvent updates the room list when a new event arrives
func (w *RoomList) NewEvent(event matrix.Event) {
	var room matrix.Room

	switch event := event.(type) {
	case matrix.RoomEvent:
		room = event.Room
	case matrix.StateEvent:
		room = event.Room
	default:
		return
	}

	for i, elem := range w.rooms {
		if elem.room == room {
			w.rooms = append(w.rooms[:i], w.rooms[i+1:]...)
			break
		}
	}
	w.rooms = append([]roomListElement{NewRoomListElement(room, event)}, w.rooms...)
}

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

// Layout implements the layout.Widget interface
func (w *RoomList) Layout(gtx Gtx) Dims {
	return w.list.Layout(gtx, len(w.rooms), func(gtx Gtx, index int) Dims {
		return w.rooms[index].Layout(gtx)
	})
}

// roomListElement is a element in the room list
type roomListElement struct {
	room      matrix.Room
	lastEvent matrix.Event
	click     *gesture.Click
}

func NewRoomListElement(room matrix.Room, event matrix.Event) roomListElement {
	return roomListElement{
		room:      room,
		lastEvent: event,
		click:     new(gesture.Click),
	}
}

func (w *roomListElement) Layout(gtx Gtx) Dims {
	in := layout.Inset{
		Left:  unit.Dp(16),
		Right: unit.Dp(16),
	}

	return in.Layout(gtx, func(gtx Gtx) Dims {
		in := layout.Inset{Top: unit.Dp(16), Bottom: unit.Dp(16)}
		dims := in.Layout(gtx, func(gtx Gtx) Dims {
			return centerRowOpts().Layout(gtx,
				layout.Rigid(w.layoutIcon),
				layout.Rigid(func(gtx Gtx) Dims {
					return column().Layout(gtx,
						layout.Rigid(w.layoutUpper),
						layout.Rigid(w.layoutLastEvent),
					)
				}),
			)
		})
		pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
		w.click.Add(gtx.Ops)
		return dims
	})
}

func (w *roomListElement) layoutIcon(gtx Gtx) Dims {
	in := layout.Inset{Right: unit.Dp(12)}

	var initial string
	for _, c := range w.room.Displayname() {
		initial = string(unicode.ToUpper(c))
		break
	}

	return in.Layout(gtx, InitialSign{Initial: initial}.Layout)
}

func (w *roomListElement) layoutUpper(gtx Gtx) Dims {
	bgtextcol := rgb(0xbbbbbb)
	return baseline().Layout(gtx,
		layout.Rigid(func(gtx Gtx) Dims {
			lbl := material.H6(theme, w.room.Displayname())
			lbl.MaxLines = 1
			return lbl.Layout(gtx)
		}),
		layout.Flexed(1, func(gtx Gtx) Dims {
			gtx.Constraints.Min.X = gtx.Constraints.Max.X
			in := layout.Inset{Left: unit.Dp(2)}
			return in.Layout(gtx, func(gtx Gtx) Dims {
				lbl := material.Caption(theme, formatTime(eventTime(w.lastEvent)))
				lbl.MaxLines = 1
				lbl.Color = bgtextcol
				lbl.Alignment = text.End
				return lbl.Layout(gtx)
			})
		}),
	)
}

func (w *roomListElement) layoutLastEvent(gtx Gtx) Dims {
	bgtextcol := rgb(0xbbbbbb)
	in := layout.Inset{Top: unit.Dp(6)}
	return in.Layout(gtx, func(gtx Gtx) Dims {
		lbl := material.Body2(theme, formatEvent(w.lastEvent))
		lbl.Color = bgtextcol
		lbl.MaxLines = 1
		return lbl.Layout(gtx)
	})
}

// RoomHistory is a widget that displays the chat history of a room
type RoomHistory struct {
	cli    *matrix.Client
	room   matrix.Room
	list   *layout.List
	events []matrix.Event
}

func NewRoomHistory(cli *matrix.Client, room matrix.Room) *RoomHistory {
	return &RoomHistory{
		cli: cli,
		room:   room,
		events: make([]matrix.Event, 0),
		list:   &layout.List{Axis: layout.Vertical, ScrollToEnd: true},
	}
}

func (w *RoomHistory) AddEvent(event matrix.Event) {
	w.events = append(w.events, event)
}

// Layout implements the layout.Widget interface
func (w *RoomHistory) Layout(gtx Gtx) Dims {
	if w.list == nil {
		return Dims{}
	}
	return w.list.Layout(gtx, len(w.events), w.element)
}

func (w *RoomHistory) element(gtx Gtx, index int) Dims {
	msg := w.events[index]
	in := layout.Inset{Top: unit.Dp(16), Left: unit.Dp(16), Right: unit.Dp(40)}
	align := layout.W
	msgCol := rgb(0xffffff)
	bgcol := theme.Color.Primary
	timecol := argb(0xaaaaaaaa)
	if ownEvent(*w.cli.User(), msg) {
	        in.Left, in.Right = in.Right, in.Left
	        align = layout.E
	        bgcol = rgb(0xeeeeee)
	        msgCol = theme.Color.Text
	        timecol = rgb(0x888888)
	}
	// in.Left = unit.Max(gtx, in.Left, w.env.insets.Left)
	// in.Right = unit.Max(gtx, in.Right, p.env.insets.Right)
	return in.Layout(gtx, func(gtx Gtx) Dims {
		return align.Layout(gtx, func(gtx Gtx) Dims {
			bg := Background{
				Color:  bgcol,
				Inset:  layout.Inset{Top: unit.Dp(8), Bottom: unit.Dp(8), Left: unit.Dp(12), Right: unit.Dp(12)},
				Radius: unit.Dp(10),
			}
			return bg.Layout(gtx, func(gtx Gtx) Dims {
				var msgWidth int
				return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
					layout.Rigid(func(gtx Gtx) Dims {
						lbl := material.Body2(theme, formatEvent(msg))
						lbl.Color = msgCol
						dims := lbl.Layout(gtx)
						dims.Size.Y += gtx.Px(unit.Dp(4))
						msgWidth = dims.Size.X
						return dims
					}),

					layout.Rigid(func(gtx Gtx) Dims {
						gtx.Constraints.Min.X = msgWidth
						f := layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween, Alignment: layout.Middle}

						var children []layout.FlexChild
						child := layout.Rigid(func(gtx Gtx) Dims {
							// time := formatTime(msg.OriginServerTS)
							time := formatTime(time.Time{})
							lbl := material.Caption(theme, time)
							lbl.Color = timecol
							return lbl.Layout(gtx)
						})
						children = append(children, child)

						// if msg.Own {
						//         child := layout.Rigid(func(gtx Gtx) Dims {
						//                 in := layout.Inset{Left: unit.Dp(12)}
						//                 return in.Layout(gtx, func(gtx Gtx) Dims {
						//                         checkmark := p.checkmark.image(gtx, timecol)
						//                         sz := checkmark.Size()
						//                         if msg.Sent {
						//                                 checkmark.Add(gtx.Ops)
						//                                 paint.PaintOp{Rect: f32.Rectangle{Max: toPointF(sz)}}.Add(gtx.Ops)
						//                         }
						//                         return layout.Dimensions{Size: sz}
						//                 })
						//         })
						//         children = append(children, child)
						// }
						return f.Layout(gtx, children...)
					}),
				)
			})
		})
	})
}

func formatEvent(event matrix.Event) string {
	switch event := event.(type) {
	case matrix.RoomEvent:
		switch event.Type {
		case "m.room.message":
			if event.Content["msgtype"] == "m.text" {
				return fmt.Sprintf("%s: %s", event.Sender.Displayname(), event.Content["body"])
			}
			return fmt.Sprintf("Unsupported message type: %s", event.Content["msgtype"])
		}
	case matrix.StateEvent:
	}

	return fmt.Sprintf("%#v", event)
}

func ownEvent(me matrix.User, ev matrix.Event) bool {
	switch ev := ev.(type) {
	case matrix.AccountDataEvent:
		return true
	case matrix.RoomEvent:
		return ev.Sender.ID == me.ID
	case matrix.StateEvent:
		return ev.Sender.ID == me.ID
	}

	return false
}

A room_page.go => room_page.go +174 -0
@@ 0,0 1,174 @@
package main

import (
	"time"
	log "github.com/sirupsen/logrus"

	"gioui.org/layout"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/matrix"
)

type RoomPage struct {
	rx, tx chan Event

	matrixEvents chan matrix.Event

	cli  *matrix.Client
	room matrix.Room

	roomHistory *RoomHistory
}

func NewRoomPage(cli *matrix.Client, room matrix.Room) *RoomPage {
	return &RoomPage{
		cli:          cli,
		matrixEvents: make(chan matrix.Event, 100),
		room:         room,
		roomHistory:  NewRoomHistory(cli.User().ID),
	}
}

func (r *RoomPage) Start(rx, tx chan Event) {
	if r.cli == nil {
		return 
	}

	r.rx, r.tx = rx, tx

	r.room.Notify(r.matrixEvents)
}

func (r *RoomPage) Layout(gtx Gtx) Dims {
	if r.cli == nil {
		return Dims{}
	}

	r.update()

	return r.roomHistory.Layout(gtx)
}

func (r *RoomPage) update() {
	for {
		select {
		case event := <-r.rx:
			log.WithField("page", "room").Warnf("%#v", event)
		case event := <-r.matrixEvents:
			r.roomHistory.AddEvent(event)
		default:
			return
		}
	}
}

func (r *RoomPage) Stop() {
}

// RoomHistory is a widget that displays the chat history of a room
type RoomHistory struct {
	user   string
	list   *layout.List
	events []matrix.Event
}

func NewRoomHistory(user string) *RoomHistory {
	return &RoomHistory{
		user:   user,
		events: make([]matrix.Event, 0),
		list:   &layout.List{Axis: layout.Vertical, ScrollToEnd: true},
	}
}

func (w *RoomHistory) AddEvent(event matrix.Event) {
	w.events = append(w.events, event)
}

// Layout implements the layout.Widget interface
func (w *RoomHistory) Layout(gtx Gtx) Dims {
	if w.list == nil {
		return Dims{}
	}
	return w.list.Layout(gtx, len(w.events), w.element)
}

func (w *RoomHistory) element(gtx Gtx, index int) Dims {
	msg := w.events[index]
	in := layout.Inset{Top: unit.Dp(16), Left: unit.Dp(16), Right: unit.Dp(40)}
	align := layout.W
	msgCol := rgb(0xffffff)
	bgcol := theme.Color.Primary
	timecol := argb(0xaaaaaaaa)
	if ownEvent(w.user, msg) {
		in.Left, in.Right = in.Right, in.Left
		align = layout.E
		bgcol = rgb(0xeeeeee)
		msgCol = theme.Color.Text
		timecol = rgb(0x888888)
	}
	// in.Left = unit.Max(gtx, in.Left, w.env.insets.Left)
	// in.Right = unit.Max(gtx, in.Right, p.env.insets.Right)
	return in.Layout(gtx, func(gtx Gtx) Dims {
		return align.Layout(gtx, func(gtx Gtx) Dims {
			bg := Background{
				Color:  bgcol,
				Inset:  layout.Inset{Top: unit.Dp(8), Bottom: unit.Dp(8), Left: unit.Dp(12), Right: unit.Dp(12)},
				Radius: unit.Dp(10),
			}
			return bg.Layout(gtx, func(gtx Gtx) Dims {
				var msgWidth int
				return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
					layout.Rigid(func(gtx Gtx) Dims {
						lbl := material.Body2(theme, formatEvent(msg))
						lbl.Color = msgCol
						dims := lbl.Layout(gtx)
						dims.Size.Y += gtx.Px(unit.Dp(4))
						msgWidth = dims.Size.X
						return dims
					}),

					layout.Rigid(func(gtx Gtx) Dims {
						gtx.Constraints.Min.X = msgWidth
						f := layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween, Alignment: layout.Middle}

						var children []layout.FlexChild
						child := layout.Rigid(func(gtx Gtx) Dims {
							time := formatTime(timeEvent(msg))
							lbl := material.Caption(theme, time)
							lbl.Color = timecol
							return lbl.Layout(gtx)
						})
						children = append(children, child)

						return f.Layout(gtx, children...)
					}),
				)
			})
		})
	})
}

func ownEvent(me string, ev matrix.Event) bool {
	switch ev := ev.(type) {
	case matrix.AccountDataEvent:
		return true
	case matrix.RoomEvent:
		return ev.Sender.ID == me
	case matrix.StateEvent:
		return ev.Sender.ID == me
	}

	return false
}

func timeEvent(ev matrix.Event) time.Time {
	switch ev := ev.(type) {
	case matrix.RoomEvent:
		return ev.OriginServerTS
	case matrix.StateEvent:
		return ev.OriginServerTS
	default:
		return time.Time{}
	}
}