~f4814n/frost

cc0c6f22a84ae80044f68163c5a6ae2e7285ded0 — Fabian Geiselhart 1 year, 4 months ago 9b1f94b
Room page. Allows reading history and sending events.
5 files changed, 413 insertions(+), 2 deletions(-)

M default_page.go
M go.mod
M main.go
A room_page.go
M ui.go
M default_page.go => default_page.go +22 -2
@@ 9,6 9,8 @@ import (
	"unicode"

	"gioui.org/f32"
	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"


@@ 28,11 30,16 @@ var contactColors = []color.RGBA{
	{A: 0xff, R: 0x00, G: 0x89, B: 0x7b},
}

type ShowRoomEvent struct {
	Room matrix.Room
}

type DefaultPage struct {
	env *Env

	list  *layout.List
	rooms []room
	list   *layout.List
	rooms  []room
	clicks []gesture.Click

	mut sync.Mutex
}


@@ 84,6 91,7 @@ func (p *DefaultPage) Start(ctx context.Context) {
					if event.Type == "m.room.create" {
						p.mut.Lock()
						p.rooms = append(p.rooms, room{Room: event.Room})
						p.clicks = make([]gesture.Click, len(p.rooms))
						p.mut.Unlock()
						p.env.redraw()
					}


@@ 113,6 121,15 @@ func (p *DefaultPage) Start(ctx context.Context) {
}

func (p *DefaultPage) Event(gtx *layout.Context) interface{} {
	for i := range p.clicks {
		click := &p.clicks[i]
		for _, e := range click.Events(gtx) {
			if e.Type == gesture.TypeClick {
				room := p.rooms[i]
				return ShowRoomEvent{Room: room.Room}
			}
		}
	}
	return nil
}



@@ 228,6 245,9 @@ func (p *DefaultPage) layoutRoom(gtx *layout.Context, index int) {
			)
		})
	})
	click := &p.clicks[index]
	pointer.Rect(image.Rectangle{Max: gtx.Dimensions.Size}).Add(gtx.Ops)
	click.Add(gtx.Ops)
}

type clipCircle struct {

M go.mod => go.mod +1 -0
@@ 10,6 10,7 @@ require (
	github.com/kr/text v0.2.0 // indirect
	github.com/sirupsen/logrus v1.5.0
	github.com/stretchr/testify v1.5.1 // indirect
	golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3
	golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // indirect
	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
	gopkg.in/yaml.v2 v2.2.8 // indirect

M main.go => main.go +8 -0
@@ 106,6 106,14 @@ func (a *App) update(gtx *layout.Context) {
				}
				a.env.redraw()
			}()
		case ShowRoomEvent:
			go func() {
				a.component = newRoomPage(&a.env, e.Room)
				a.component.Start(context.Background())
			}()
		case BackEvent:
			a.component = newDefaultPage(&a.env)
			a.component.Start(context.Background())
		default:
			log.Warnf("Unhandled event:%#v", e)
		}

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

import (
	"context"
	"fmt"
	log "github.com/sirupsen/logrus"
	"image/color"
	"time"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
	"git.sr.ht/~f4814n/matrix"
)

type roomPage struct {
	env    *Env
	room   matrix.Room
	notify chan matrix.Event
	list   *layout.List
	events []matrix.Event
	topbar *Topbar

	msgEdit *widget.Editor
	send    *widget.Button
}

func newRoomPage(env *Env, room matrix.Room) *roomPage {
	return &roomPage{
		env:    env,
		room:   room,
		notify: make(chan matrix.Event),
		msgEdit: &widget.Editor{
			Submit: true,
		},
		send: new(widget.Button),
		list: &layout.List{
			Axis:        layout.Vertical,
			ScrollToEnd: true,
		},
		topbar: &Topbar{
			Back: true,
		},
	}
}

func (p *roomPage) Start(ctx context.Context) {
	p.room.Notify(p.notify)
	go func() {
		history, err := p.room.History()
		fmt.Printf("%#v\n", err)
		for history.Next() {
			for i, j := 0, len(history.Events)-1; i < j; i, j = i+1, j-1 {
				history.Events[i], history.Events[j] = history.Events[j], history.Events[i]
			}
			p.events = append(history.Events, p.events...)
		}
	}()

	go func() {
		for e := range p.notify {
			p.events = append(p.events, e)
		}
	}()
}

func (p *roomPage) Event(gtx *layout.Context) interface{} {
	for _, e := range p.msgEdit.Events(gtx) {
		if _, ok := e.(widget.SubmitEvent); ok {
			go p.sendEvent()
		}
	}
	if p.send.Clicked(gtx) {
		go p.sendEvent()
	}
	return p.topbar.Event(gtx)
}

func (p *roomPage) sendEvent() {
	if t := p.msgEdit.Text(); t != "" {
		_, err := p.room.SendRoomEvent("m.room.message", map[string]interface{}{"body": t, "msgtype": "m.text"})
		if err != nil {
			log.WithField("error", err).Warn("Failed to send event")
		}
		p.msgEdit.SetText("")
	}
}

func (p *roomPage) Layout(gtx *layout.Context) {
	layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func() {
			p.topbar.Layout(gtx, p.env.inset, func() {
				lbl := theme.H6(p.room.Displayname())
				lbl.Color = rgb(0xffffff)
				lbl.Layout(gtx)
			})
		}),
		layout.Flexed(1, func() {
			gtx.Constraints.Height.Min = gtx.Constraints.Height.Max
			p.list.Layout(gtx, len(p.events), func(i int) {
				p.layoutEvent(gtx, i)
			})
		}),

		layout.Rigid(func() {
			in := layout.Inset{
				Top:    unit.Dp(16),
				Left:   unit.Max(gtx, unit.Dp(16), p.env.inset.Left),
				Right:  unit.Max(gtx, unit.Dp(16), p.env.inset.Right),
				Bottom: unit.Max(gtx, unit.Dp(16), p.env.inset.Bottom),
			}
			in.Layout(gtx, func() {
				p.layoutMessageBox(gtx)
			})
		}),
	)
}

func (p *roomPage) layoutMessageBox(gtx *layout.Context) {
	if mh := gtx.Px(unit.Dp(100)); gtx.Constraints.Height.Max > mh {
		gtx.Constraints.Height.Max = mh
	}

	var sendHeight int
	layout.Flex{Alignment: layout.End}.Layout(gtx,
		layout.Flexed(1, func() {
			gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
			if gtx.Constraints.Height.Min < sendHeight {
				gtx.Constraints.Height.Min = sendHeight
			}
			bg := Background{
				Color:  rgb(0xeeeeee),
				Inset:  layout.Inset{Left: unit.Dp(8), Right: unit.Dp(8)},
				Radius: unit.Dp(10),
			}
			bg.Layout(gtx, func() {
				layout.W.Layout(gtx, func() {
					gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
					ed := theme.Editor("Send a message")
					ed.TextSize = unit.Sp(14)
					ed.Layout(gtx, p.msgEdit)
				})
			})
		}),
		layout.Rigid(func() {
			in := layout.Inset{Left: unit.Dp(8)}
			in.Layout(gtx, func() {
				btn := theme.Button("->")
				// btn.Size = unit.Dp(48)
				// btn.Padding = unit.Dp(12)
				btn.Layout(gtx, p.send)
				sendHeight = gtx.Dimensions.Size.Y
			})
		}),
	)

}

func (p *roomPage) layoutEvent(gtx *layout.Context, index int) {
	event := p.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 event, ok := event.(matrix.RoomEvent); ok {
		if event.Sender.User.ID == p.env.cli.User().ID {
			in.Left, in.Right = in.Right, in.Left
			align = layout.E
			bgcol = rgb(0xeeeeee)
			msgCol = theme.Color.Text
			timecol = rgb(0x888888)
		}

		// TODO: State events

		in.Left = unit.Max(gtx, in.Left, p.env.inset.Left)
		in.Right = unit.Max(gtx, in.Right, p.env.inset.Right)

		in.Layout(gtx, func() {
			align.Layout(gtx, func() {
				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),
				}
				bg.Layout(gtx, func() {
					var msgWidth int
					layout.Flex{Axis: layout.Vertical}.Layout(gtx,
						layout.Rigid(func() {
							lbl := theme.Body2(formatEvent(event))
							lbl.Color = msgCol
							lbl.Layout(gtx)
							gtx.Dimensions.Size.Y += gtx.Px(unit.Dp(4))
							msgWidth = gtx.Dimensions.Size.X
						}),
						layout.Rigid(func() {
							gtx.Constraints.Width.Min = msgWidth
							f := layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween, Alignment: layout.Middle}

							var children []layout.FlexChild
							child := layout.Rigid(func() {
								time := formatTime(event.OriginServerTS)
								lbl := theme.Caption(time)
								lbl.Color = timecol
								lbl.Layout(gtx)
							})
							children = append(children, child)

							// TODO Checkmark
							// if msg.Own {
							// 	child = layout.Rigid(func() {
							// 		in := layout.Inset{Left: unit.Dp(12)}
							// 		in.Layout(gtx, func() {
							// 			checkmark := p.checkmark
							// 		})
							// 	})
							// }
							f.Layout(gtx, children...)
						}),
					)
				})
			})
		})
	}
}

type Background struct {
	Color  color.RGBA
	Radius unit.Value
	Inset  layout.Inset
}

func (b *Background) Layout(gtx *layout.Context, w layout.Widget) {
	var macro op.MacroOp
	macro.Record(gtx.Ops)
	b.Inset.Layout(gtx, w)
	macro.Stop()
	var stack op.StackOp
	stack.Push(gtx.Ops)
	size := gtx.Dimensions.Size
	width, height := float32(size.X), float32(size.Y)
	if r := float32(gtx.Px(b.Radius)); r > 0 {
		if r > width/2 {
			r = width / 2
		}
		if r > height/2 {
			r = height / 2
		}
		clip.Rect{
			Rect: f32.Rectangle{Max: f32.Point{
				X: width, Y: height,
			}}, NW: r, NE: r, SW: r, SE: r,
		}.Op(gtx.Ops).Add(gtx.Ops)
	}
	paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
	paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: width, Y: height}}}.Add(gtx.Ops)
	macro.Add()
	stack.Pop()
}

func formatTime(t time.Time) string {
	y, m, d := t.Date()
	tday := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
	y, m, d = time.Now().Date()
	nday := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
	n := int(nday.Sub(tday) / (time.Hour * 24))
	format := "Jan _2 15:04"
	if n < 7 {
		format = "Mon 15:04"
	}
	return t.Format(format)
}

func formatEvent(e matrix.Event) string {
	switch e := e.(type) {
	case matrix.RoomEvent:
		if e.Type == "m.room.message" {
			return e.Content["body"].(string)
		}
		return fmt.Sprintf("%#v", e)
	default:
		return fmt.Sprintf("%#v", e)
	}
}

M ui.go => ui.go +92 -0
@@ 2,10 2,21 @@ package main

import (
	"context"
	"image"
	"image/color"
	"image/draw"
	"net/http"

	"gioui.org/f32"
	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"git.sr.ht/~f4814n/matrix"
	"golang.org/x/exp/shiny/iconvg"
	"golang.org/x/exp/shiny/materialdesign/icons"

	membackend "git.sr.ht/~f4814n/matrix/backend/memory"
)



@@ 43,3 54,84 @@ func (p *LoadingPage) Event(gtx *layout.Context) interface{} {

func (p *LoadingPage) Layout(gtx *layout.Context) {
}

type Topbar struct {
	Back bool

	backClick gesture.Click
}

func (t *Topbar) Event(gtx *layout.Context) interface{} {
	for _, e := range t.backClick.Events(gtx) {
		if e.Type == gesture.TypeClick {
			return BackEvent{}
		}
	}
	return nil
}

type BackEvent struct{}

func (t *Topbar) Layout(gtx *layout.Context, insets layout.Inset, w layout.Widget) {
	insets = layout.Inset{
		Top:    unit.Add(gtx, insets.Top, unit.Dp(16)),
		Bottom: unit.Dp(16),
		Left:   unit.Max(gtx, insets.Left, unit.Dp(16)),
		Right:  unit.Max(gtx, insets.Right, unit.Dp(16)),
	}
	layout.Stack{Alignment: layout.SW}.Layout(gtx,
		layout.Expanded(func() {
			fill{theme.Color.Primary}.Layout(gtx)
		}),
		layout.Stacked(func() {
			insets.Layout(gtx, func() {
				layout.Flex{Alignment: layout.Middle}.Layout(gtx,
					layout.Rigid(func() {
						if t.Back {
							ico := (&icon{src: icons.NavigationArrowBack, size: unit.Dp(24)}).image(gtx, rgb(0xffffff))
							ico.Add(gtx.Ops)
							paint.PaintOp{Rect: f32.Rectangle{Max: toPointF(ico.Size())}}.Add(gtx.Ops)
							gtx.Dimensions.Size = ico.Size()
							gtx.Dimensions.Size.X += gtx.Px(unit.Dp(4))
							pointer.Rect(image.Rectangle{Max: gtx.Dimensions.Size}).Add(gtx.Ops)
							t.backClick.Add(gtx.Ops)
						}
					}),
					layout.Flexed(1, w),
				)
			})
		}),
	)
}

func toPointF(p image.Point) f32.Point {
	return f32.Point{X: float32(p.X), Y: float32(p.Y)}
}

type icon struct {
	src  []byte
	size unit.Value

	// Cached values.
	op      paint.ImageOp
	imgSize int
}

func (ic *icon) image(c unit.Converter, col color.RGBA) paint.ImageOp {
	sz := c.Px(ic.size)
	if sz == ic.imgSize {
		return ic.op
	}
	m, _ := iconvg.DecodeMetadata(ic.src)
	dx, dy := m.ViewBox.AspectRatio()
	img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz, Y: int(float32(sz) * dy / dx)}})
	var ico iconvg.Rasterizer
	ico.SetDstImage(img, img.Bounds(), draw.Src)
	m.Palette[0] = col
	iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{
		Palette: &m.Palette,
	})
	ic.op = paint.NewImageOp(img)
	ic.imgSize = sz
	return ic.op
}