ABSTRACTIONS! THEY'RE EVERYWHERE!

This commit is contained in:
diamondburned (Forefront) 2020-06-06 00:44:36 -07:00
parent 0171ac6b52
commit 2d628fbbb3
24 changed files with 793 additions and 277 deletions

5
go.mod
View File

@ -8,9 +8,14 @@ require (
github.com/Xuanwo/go-locale v0.2.0
github.com/diamondburned/cchat v0.0.15
github.com/diamondburned/cchat-mock v0.0.0-20200605224934-31a53c555ea2
github.com/diamondburned/imgutil v0.0.0-20200606035324-63abbc0fdea6
github.com/die-net/lrucache v0.0.0-20190707192454-883874fe3947
github.com/goodsign/monday v1.0.0
github.com/google/btree v1.0.0 // indirect
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
github.com/markbates/pkger v0.17.0
github.com/peterbourgon/diskv v2.0.1+incompatible
github.com/pkg/errors v0.9.1
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
)

16
go.sum
View File

@ -9,6 +9,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diamondburned/cchat v0.0.15 h1:1o4OX8zw/CdSv3Idaylz7vjHVOZKEi/xkg8BpEvtsHY=
github.com/diamondburned/cchat v0.0.15/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/imgutil v0.0.0-20200606035324-63abbc0fdea6 h1:APALM1hskCByjOVW9CoUwjg0TIJgKZ62dgFr/9soqss=
github.com/diamondburned/imgutil v0.0.0-20200606035324-63abbc0fdea6/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
github.com/die-net/lrucache v0.0.0-20190707192454-883874fe3947 h1:U/5Sq2nJQ0XDyks+8ATghtHSuquIGq7JYrqSrvtR2dg=
github.com/die-net/lrucache v0.0.0-20190707192454-883874fe3947/go.mod h1:KsMcjmY1UCGl7ozPbdVPDOvLaFeXnptSvtNRczhxNto=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
@ -17,10 +23,14 @@ github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194 h1:bB6XWpxMt2isCWqzjXN8tfVazjxvD8nRJrNoKcL0xAc=
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -30,6 +40,9 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/markbates/pkger v0.17.0 h1:RFfyBPufP2V6cddUyyEVSHBpaAnM1WzaMNyqomeT+iY=
github.com/markbates/pkger v0.17.0/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/peterbourgon/diskv v1.0.0 h1:bRU92KzrX3TQ6IYobfie/PnZkFC+1opBfHpf/PHPDoo=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -40,11 +53,14 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717 h1:3M/uUZajYn/082wzUajekePxpUAZhMTfXvI9R+26SJ0=
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -0,0 +1,94 @@
package httputil
import (
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/die-net/lrucache"
"github.com/gregjones/httpcache"
"github.com/gregjones/httpcache/diskcache"
"github.com/peterbourgon/diskv"
"github.com/pkg/errors"
)
var dskcached *http.Client
var memcached *http.Client
func init() {
var basePath = filepath.Join(os.TempDir(), "cchat-gtk-pridemonth")
http.DefaultClient.Timeout = 15 * time.Second
dskcached = &(*http.DefaultClient)
dskcached.Transport = httpcache.NewTransport(
diskcache.NewWithDiskv(diskv.New(diskv.Options{
BasePath: basePath,
TempDir: filepath.Join(basePath, "tmp"),
PathPerm: 0750,
FilePerm: 0750,
Compression: diskv.NewZlibCompressionLevel(2),
CacheSizeMax: 25 * 1024 * 1024, // 25 MiB in memory
})),
)
memcached = &(*http.DefaultClient)
memcached.Transport = httpcache.NewTransport(lrucache.New(
25*1024*1024, // 25 MiB in memory
secs(2*time.Hour), // 2 hours cache
))
}
func secs(dura time.Duration) int64 {
return int64(dura / time.Second)
}
func AsyncStreamUncached(url string, fn func(r io.Reader)) {
gts.Async(func() (func(), error) {
r, err := get(url, false)
if err != nil {
return nil, err
}
return func() {
fn(r.Body)
r.Body.Close()
}, nil
})
}
func AsyncStream(url string, fn func(r io.Reader)) {
gts.Async(func() (func(), error) {
r, err := get(url, true)
if err != nil {
return nil, err
}
return func() {
fn(r.Body)
r.Body.Close()
}, nil
})
}
func get(url string, cached bool) (r *http.Response, err error) {
if cached {
r, err = dskcached.Get(url)
} else {
r, err = memcached.Get(url)
}
if err != nil {
return nil, err
}
if r.StatusCode < 200 || r.StatusCode > 299 {
r.Body.Close()
return nil, errors.Errorf("Unexpected status %d", r.StatusCode)
}
return r, nil
}

View File

@ -0,0 +1,85 @@
package httputil
import (
"io"
"strings"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
// AsyncImage loads an image. This method uses the cache.
func AsyncImage(img *gtk.Image, url string, procs ...imgutil.Processor) {
go asyncImage(img, url, procs...)
}
func asyncImage(img *gtk.Image, url string, procs ...imgutil.Processor) {
r, err := get(url, true)
if err != nil {
log.Error(err)
return
}
defer r.Body.Close()
l, err := gdk.PixbufLoaderNew()
if err != nil {
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
return
}
gif := strings.Contains(url, ".gif")
// This is a very important signal, so we must do it synchronously. Gotk3's
// callback implementation requires all connects to be synchronous to a
// certain thread.
gts.ExecSync(func() {
l.Connect("area-prepared", func() {
if gif {
p, err := l.GetPixbuf()
if err != nil {
log.Error(errors.Wrap(err, "Failed to get pixbuf"))
return
}
img.SetFromPixbuf(p)
} else {
p, err := l.GetAnimation()
if err != nil {
log.Error(errors.Wrap(err, "Failed to get animation"))
return
}
img.SetFromAnimation(p)
}
})
})
// If we have processors, then write directly in there.
if len(procs) > 0 {
if !gif {
err = imgutil.ProcessStream(l, r.Body, procs)
} else {
err = imgutil.ProcessAnimationStream(l, r.Body, procs)
}
} else {
// Else, directly copy the body over.
_, err = io.Copy(l, r.Body)
}
if err != nil {
log.Error(errors.Wrap(err, "Error processing image"))
return
}
if err := l.Close(); err != nil {
log.Error(errors.Wrap(err, "Failed to close pixbuf"))
}
}
// AsyncImageSized resizes using GdkPixbuf. This method does not use the cache.
func AsyncImageSized(img *gtk.Image, url string, w, h int, procs ...imgutil.Processor) {
// TODO
panic("TODO")
}

View File

@ -1,156 +0,0 @@
package compact
import (
"fmt"
"html"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/message/autoscroll"
"github.com/diamondburned/cchat-gtk/internal/ui/message/input"
"github.com/gotk3/gotk3/gtk"
)
type Container struct {
*autoscroll.ScrolledWindow
main *gtk.Grid
messages map[string]*Message
nonceMsgs map[string]*Message
bottomed bool
}
func NewContainer() *Container {
grid, _ := gtk.GridNew()
grid.SetColumnSpacing(10)
grid.SetRowSpacing(5)
grid.SetMarginStart(5)
grid.SetMarginEnd(5)
grid.SetMarginBottom(5)
grid.Show()
sw := autoscroll.NewScrolledWindow()
sw.Add(grid)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
sw.Show()
container := Container{
ScrolledWindow: sw,
main: grid,
messages: map[string]*Message{},
nonceMsgs: map[string]*Message{},
bottomed: true, // bottomed by default.
}
return &container
}
func (c *Container) Reset() {
// does this actually work?
var rows = c.len()
for i := 0; i < rows; i++ {
c.main.RemoveRow(i)
}
c.messages = map[string]*Message{}
c.nonceMsgs = map[string]*Message{}
// default to being bottomed
c.bottomed = true
}
func (c *Container) len() int {
return len(c.messages) + len(c.nonceMsgs)
}
// PresendMessage is not thread-safe.
func (c *Container) PresendMessage(msg input.PresendMessage) func(error) {
msgc := NewPresendMessage(msg.Content(), msg.Author(), msg.AuthorID(), msg.Nonce())
msgc.index = c.len()
c.nonceMsgs[msgc.Nonce] = &msgc
msgc.Attach(c.main, msgc.index)
return func(err error) {
msgc.SetSensitive(true)
// Did we fail?
if err != nil {
msgc.Content.SetMarkup(fmt.Sprintf(
`<span color="red">%s</span>`,
html.EscapeString(msgc.Content.GetLabel()),
))
msgc.Content.SetTooltipText(err.Error())
}
}
}
// FindMessage is not thread-safe.
func (c *Container) FindMessage(msg cchat.MessageHeader) *Message {
// Search using the ID first.
m, ok := c.messages[msg.ID()]
if ok {
return m
}
// Is this an existing message?
if noncer, ok := msg.(cchat.MessageNonce); ok {
var nonce = noncer.Nonce()
m, ok := c.nonceMsgs[nonce]
if ok {
// Move the message outside nonceMsgs.
delete(c.nonceMsgs, nonce)
c.messages[msg.ID()] = m
// Set the right ID.
m.ID = msg.ID()
return m
}
}
return nil
}
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
// Attempt update before insert (aka upsert).
if msgc := c.FindMessage(msg); msgc != nil {
msgc.SetSensitive(true)
msgc.UpdateAuthor(msg.Author())
msgc.UpdateContent(msg.Content())
msgc.UpdateTimestamp(msg.Time())
return
}
msgc := NewMessage(msg)
msgc.index = c.len() // unsure
c.messages[msgc.ID] = &msgc
msgc.Attach(c.main, msgc.index)
})
}
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
gts.ExecAsync(func() {
if msgc := c.FindMessage(msg); msgc != nil {
if author := msg.Author(); author != nil {
msgc.UpdateAuthor(author)
}
if content := msg.Content(); !content.Empty() {
msgc.UpdateContent(content)
}
}
})
}
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
gts.ExecAsync(func() {
// TODO: add nonce check.
if m, ok := c.messages[msg.ID()]; ok {
delete(c.messages, msg.ID())
c.main.RemoveRow(m.index)
}
})
}

View File

@ -1 +0,0 @@
package cozy

View File

@ -5,7 +5,7 @@ import "github.com/gotk3/gotk3/gtk"
type ScrolledWindow struct {
gtk.ScrolledWindow
vadj gtk.Adjustment
bottomed bool // :floshed:
Bottomed bool // :floshed:
}
func NewScrolledWindow() *ScrolledWindow {
@ -15,22 +15,18 @@ func NewScrolledWindow() *ScrolledWindow {
sw := &ScrolledWindow{*gtksw, *gtksw.GetVAdjustment(), true} // bottomed by default
sw.Connect("size-allocate", func(_ *gtk.ScrolledWindow) {
// We can't really trust Gtk to be competent.
if sw.bottomed {
if sw.Bottomed {
sw.ScrollToBottom()
}
})
sw.vadj.Connect("value-changed", func(adj *gtk.Adjustment) {
// Manually check if we're anchored on scroll.
sw.bottomed = (adj.GetUpper() - adj.GetPageSize()) <= adj.GetValue()
sw.Bottomed = (adj.GetUpper() - adj.GetPageSize()) <= adj.GetValue()
})
return sw
}
func (s *ScrolledWindow) Bottomed() bool {
return s.bottomed
}
// GetVAdjustment overrides gtk.ScrolledWindow's.
func (s *ScrolledWindow) GetVAdjustment() *gtk.Adjustment {
return &s.vadj

View File

@ -0,0 +1,25 @@
package compact
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
)
type Container struct {
*container.GridContainer
}
func NewContainer() *Container {
c := &Container{}
c.GridContainer = container.NewGridContainer(c)
return c
}
func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
return NewMessage(msg)
}
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
return NewPresendMessage(msg)
}

View File

@ -0,0 +1,52 @@
package compact
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/gotk3/gotk3/gtk"
)
type PresendMessage struct {
*message.GenericPresendContainer
}
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
return PresendMessage{
GenericPresendContainer: message.NewPresendContainer(msg),
}
}
func (p PresendMessage) Attach(grid *gtk.Grid, row int) {
attachGenericContainer(p.GenericContainer, grid, row)
}
type Message struct {
*message.GenericContainer
}
var _ container.GridMessage = (*Message)(nil)
func NewMessage(msg cchat.MessageCreate) Message {
return Message{
GenericContainer: message.NewContainer(msg),
}
}
func NewEmptyMessage() Message {
return Message{
GenericContainer: message.NewEmptyContainer(),
}
}
// TODO: fix a bug here related to new messages overlapping
func (m Message) Attach(grid *gtk.Grid, row int) {
attachGenericContainer(m.GenericContainer, grid, row)
}
func attachGenericContainer(m *message.GenericContainer, grid *gtk.Grid, row int) {
grid.Attach(m.Timestamp, 0, row, 1, 1)
grid.Attach(m.Username, 1, row, 1, 1)
grid.Attach(m.Content, 2, row, 1, 1)
}

View File

@ -0,0 +1,210 @@
package container
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/autoscroll"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
type GridMessage interface {
message.Container
Attach(grid *gtk.Grid, row int)
}
type PresendGridMessage interface {
GridMessage
message.PresendContainer
}
// gridMessage w/ required internals
type gridMessage struct {
GridMessage
presend message.PresendContainer // this shouldn't be here but i'm lazy
index int
}
// Constructor is an interface for making custom message implementations which
// allows GridContainer to generically work with.
type Constructor interface {
NewMessage(cchat.MessageCreate) GridMessage
NewPresendMessage(input.PresendMessage) PresendGridMessage
}
// Container is a generic messages container.
type Container interface {
gtk.IWidget
cchat.MessagesContainer
Reset()
ScrollToBottom()
// PresendMessage is for unsent messages.
PresendMessage(input.PresendMessage) (done func(sendError error))
}
// GridContainer is an implementation of Container, which allows flexible
// message grids.
type GridContainer struct {
*autoscroll.ScrolledWindow
Main *gtk.Grid
construct Constructor
messages map[string]*gridMessage
nonceMsgs map[string]*gridMessage
}
var (
_ Container = (*GridContainer)(nil)
_ cchat.MessagesContainer = (*GridContainer)(nil)
)
func NewGridContainer(constr Constructor) *GridContainer {
grid, _ := gtk.GridNew()
grid.SetColumnSpacing(10)
grid.SetRowSpacing(5)
grid.SetMarginStart(5)
grid.SetMarginEnd(5)
grid.SetMarginBottom(5)
grid.Show()
sw := autoscroll.NewScrolledWindow()
sw.Add(grid)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
sw.Show()
container := GridContainer{
ScrolledWindow: sw,
Main: grid,
construct: constr,
messages: map[string]*gridMessage{},
nonceMsgs: map[string]*gridMessage{},
}
return &container
}
func (c *GridContainer) Reset() {
// does this actually work?
var rows = c.len()
for i := 0; i < rows; i++ {
c.Main.RemoveRow(i)
}
c.messages = map[string]*gridMessage{}
c.nonceMsgs = map[string]*gridMessage{}
c.ScrolledWindow.Bottomed = true
}
func (c *GridContainer) len() int {
return len(c.messages) + len(c.nonceMsgs)
}
// PresendMessage is not thread-safe.
func (c *GridContainer) PresendMessage(msg input.PresendMessage) func(error) {
presend := c.construct.NewPresendMessage(msg)
msgc := gridMessage{
GridMessage: presend,
presend: presend,
index: c.len(),
}
c.nonceMsgs[presend.Nonce()] = &msgc
msgc.Attach(c.Main, msgc.index)
return func(err error) {
if err != nil {
presend.SetSentError(err)
log.Error(errors.Wrap(err, "Failed to send message"))
}
}
}
// FindMessage is not thread-safe. This exists for backwards compatibility.
func (c *GridContainer) FindMessage(msg cchat.MessageHeader) GridMessage {
if m := c.findMessage(msg); m != nil {
return m.GridMessage
}
return nil
}
func (c *GridContainer) findMessage(msg cchat.MessageHeader) *gridMessage {
// Search using the ID first.
m, ok := c.messages[msg.ID()]
if ok {
return m
}
// Is this an existing message?
if noncer, ok := msg.(cchat.MessageNonce); ok {
var nonce = noncer.Nonce()
// Things in this map are guaranteed to have presend != nil.
m, ok := c.nonceMsgs[nonce]
if ok {
// Move the message outside nonceMsgs.
delete(c.nonceMsgs, nonce)
c.messages[msg.ID()] = m
// Set the right ID.
m.presend.SetID(msg.ID())
m.presend.SetDone()
// Destroy the presend struct.
m.presend = nil
return m
}
}
return nil
}
func (c *GridContainer) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
// Attempt update before insert (aka upsert).
if msgc := c.FindMessage(msg); msgc != nil {
msgc.UpdateAuthor(msg.Author())
msgc.UpdateContent(msg.Content())
msgc.UpdateTimestamp(msg.Time())
return
}
msgc := gridMessage{
GridMessage: c.construct.NewMessage(msg),
index: c.len(),
}
c.messages[msgc.ID()] = &msgc
msgc.Attach(c.Main, msgc.index)
})
}
func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
gts.ExecAsync(func() {
if msgc := c.FindMessage(msg); msgc != nil {
if author := msg.Author(); author != nil {
msgc.UpdateAuthor(author)
}
if content := msg.Content(); !content.Empty() {
msgc.UpdateContent(content)
}
}
})
}
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
gts.ExecAsync(func() {
// TODO: add nonce check.
if m, ok := c.messages[msg.ID()]; ok {
delete(c.messages, msg.ID())
c.Main.RemoveRow(m.index)
}
})
}

View File

@ -0,0 +1,13 @@
package cozy
import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/autoscroll"
"github.com/gotk3/gotk3/gtk"
)
type Container struct {
*autoscroll.ScrolledWindow
main *gtk.Grid
messages map[string]Message
nonceMsgs map[string]Message
}

View File

@ -0,0 +1,20 @@
package cozy
import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/gotk3/gotk3/gtk"
)
type Message interface {
gtk.IWidget
message.Container
}
type FullMessage struct {
*gtk.Box
Avatar *gtk.Image
*message.GenericContainer
}
func NewFullMessage()

View File

@ -1,4 +1,4 @@
package compact
package message
import (
"time"
@ -11,44 +11,46 @@ import (
"github.com/gotk3/gotk3/pango"
)
type Message struct {
index int
ID string
AuthorID string
Nonce string
type Container interface {
ID() string
AuthorID() string
Nonce() string
UpdateAuthor(cchat.MessageAuthor)
UpdateAuthorName(text.Rich)
UpdateContent(text.Rich)
UpdateTimestamp(time.Time)
}
// GenericContainer provides a single generic message container for subpackages
// to use.
type GenericContainer struct {
id string
authorID string
nonce string
Timestamp *gtk.Label
Username *gtk.Label
Content *gtk.Label
}
func NewMessage(msg cchat.MessageCreate) Message {
m := NewEmptyMessage()
m.ID = msg.ID()
m.UpdateTimestamp(msg.Time())
m.UpdateAuthor(msg.Author())
m.UpdateContent(msg.Content())
var _ Container = (*GenericContainer)(nil)
func NewContainer(msg cchat.MessageCreate) *GenericContainer {
c := NewEmptyContainer()
c.id = msg.ID()
c.UpdateTimestamp(msg.Time())
c.UpdateAuthor(msg.Author())
c.UpdateContent(msg.Content())
if noncer, ok := msg.(cchat.MessageNonce); ok {
m.Nonce = noncer.Nonce()
c.nonce = noncer.Nonce()
}
return m
return c
}
func NewPresendMessage(content string, author text.Rich, authorID, nonce string) Message {
msgc := NewEmptyMessage()
msgc.Nonce = nonce
msgc.AuthorID = authorID
msgc.SetSensitive(false)
msgc.UpdateContent(text.Rich{Content: content})
msgc.UpdateTimestamp(time.Now())
msgc.updateAuthorName(author)
return msgc
}
func NewEmptyMessage() Message {
func NewEmptyContainer() *GenericContainer {
ts, _ := gtk.LabelNew("")
ts.SetLineWrap(true)
ts.SetLineWrapMode(pango.WRAP_WORD)
@ -75,39 +77,39 @@ func NewEmptyMessage() Message {
content.SetSelectable(true)
content.Show()
return Message{
return &GenericContainer{
Timestamp: ts,
Username: user,
Content: content,
}
}
func (m *Message) SetSensitive(sensitive bool) {
m.Timestamp.SetSensitive(sensitive)
m.Username.SetSensitive(sensitive)
m.Content.SetSensitive(sensitive)
func (m *GenericContainer) ID() string {
return m.id
}
func (m *Message) Attach(grid *gtk.Grid, row int) {
grid.Attach(m.Timestamp, 0, row, 1, 1)
grid.Attach(m.Username, 1, row, 1, 1)
grid.Attach(m.Content, 2, row, 1, 1)
func (m *GenericContainer) AuthorID() string {
return m.authorID
}
func (m *Message) UpdateTimestamp(t time.Time) {
func (m *GenericContainer) Nonce() string {
return m.nonce
}
func (m *GenericContainer) UpdateTimestamp(t time.Time) {
m.Timestamp.SetLabel(humanize.TimeAgo(t))
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
}
func (m *Message) UpdateAuthor(author cchat.MessageAuthor) {
m.AuthorID = author.ID()
m.updateAuthorName(author.Name())
func (m *GenericContainer) UpdateAuthor(author cchat.MessageAuthor) {
m.authorID = author.ID()
m.UpdateAuthorName(author.Name())
}
func (m *Message) updateAuthorName(name text.Rich) {
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
m.Username.SetMarkup(parser.RenderMarkup(name))
}
func (m *Message) UpdateContent(content text.Rich) {
func (m *GenericContainer) UpdateContent(content text.Rich) {
m.Content.SetMarkup(parser.RenderMarkup(content))
}

View File

@ -0,0 +1,57 @@
package message
import (
"html"
"time"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat/text"
)
type PresendContainer interface {
Container
SetID(id string)
SetDone()
SetSentError(err error)
}
// PresendGenericContainer is the generic container with extra methods
// implemented for mutability of the generic message container.
type GenericPresendContainer struct {
*GenericContainer
}
var _ PresendContainer = (*GenericPresendContainer)(nil)
func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
c := NewEmptyContainer()
c.nonce = msg.Nonce()
c.authorID = msg.AuthorID()
c.UpdateContent(text.Rich{Content: msg.Content()})
c.UpdateTimestamp(time.Now())
c.UpdateAuthorName(msg.Author())
p := &GenericPresendContainer{
GenericContainer: c,
}
p.SetSensitive(false)
return p
}
func (m *GenericPresendContainer) SetID(id string) {
m.id = id
}
func (m *GenericPresendContainer) SetSensitive(sensitive bool) {
m.Content.SetSensitive(sensitive)
}
func (m *GenericPresendContainer) SetDone() {
m.SetSensitive(true)
}
func (m *GenericPresendContainer) SetSentError(err error) {
m.Content.SetMarkup(`<span color="red">` + html.EscapeString(m.Content.GetLabel()) + `</span>`)
m.Content.SetTooltipText(err.Error())
}

View File

@ -1,10 +1,11 @@
package message
package messages
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/message/compact"
"github.com/diamondburned/cchat-gtk/internal/ui/message/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/compact"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
@ -22,7 +23,7 @@ type Container interface {
type View struct {
*gtk.Box
Container Container
Container container.Container
SendInput *input.Field
current cchat.ServerMessage
@ -32,6 +33,7 @@ type View struct {
func NewView() *View {
view := &View{}
// TODO: change
view.Container = compact.NewContainer()
view.SendInput = input.NewField(view)

143
internal/ui/rich/image.go Normal file
View File

@ -0,0 +1,143 @@
package rich
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
type Icon struct {
*gtk.Revealer
Image *gtk.Image
resizer imgutil.Processor
procs []imgutil.Processor
url string // state
}
const DefaultIconSize = 16
var _ cchat.IconContainer = (*Icon)(nil)
func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
if sizepx == 0 {
sizepx = DefaultIconSize
}
img, _ := gtk.ImageNew()
img.Show()
img.SetSizeRequest(sizepx, sizepx)
rev, _ := gtk.RevealerNew()
rev.Add(img)
rev.SetRevealChild(false)
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
rev.SetTransitionDuration(50)
return &Icon{
Revealer: rev,
Image: img,
resizer: imgutil.Resize(sizepx, sizepx),
procs: procs,
}
}
// Thread-unsafe methods should only be called right after construction.
// SetPlaceholderIcon is not thread-safe.
func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
i.SetRevealChild(true)
i.SetSize(iconSzPx)
if iconName != "" {
primitives.SetImageIcon(i.Image, iconName, iconSzPx)
}
}
// SetSize is not thread-safe.
func (i *Icon) SetSize(szpx int) {
i.Image.SetSizeRequest(szpx, szpx)
i.resizer = imgutil.Resize(szpx, szpx)
}
// AddProcessors is not thread-safe.
func (i *Icon) AddProcessors(procs ...imgutil.Processor) {
i.procs = append(i.procs, procs...)
}
// SetIcon is thread-safe.
func (i *Icon) SetIcon(url string) {
gts.ExecAsync(func() { i.SetRevealChild(true) })
i.url = url
i.updateAsync()
}
func (i *Icon) updateAsync() {
httputil.AsyncImage(i.Image, i.url, imgutil.Prepend(i.resizer, i.procs)...)
}
type ToggleButtonImage struct {
gtk.ToggleButton
Labeler
cchat.IconContainer
Label *gtk.Label
Image *Icon
Box *gtk.Box
}
var (
_ gtk.IWidget = (*ToggleButton)(nil)
_ cchat.LabelContainer = (*ToggleButton)(nil)
)
func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
l := NewLabel(content)
l.Show()
i := NewIcon(0)
i.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(i, false, false, 0)
box.PackStart(l, true, true, 5)
box.Show()
b, _ := gtk.ToggleButtonNew()
b.Add(box)
return &ToggleButtonImage{
ToggleButton: *b,
Labeler: l, // easy inheritance of methods
IconContainer: i,
Label: &l.Label,
Image: i,
Box: box,
}
}
type Namer interface {
Name(cchat.LabelContainer) error
}
// Try tries to set the name from namer. It also tries Icon.
func (b *ToggleButtonImage) Try(namer Namer, desc string) {
if err := namer.Name(b); err != nil {
log.Error(errors.Wrap(err, "Failed to get name for "+desc))
b.SetLabel(text.Rich{Content: "Unknown"})
}
if iconer, ok := namer.(cchat.Icon); ok {
if err := iconer.Icon(b); err != nil {
log.Error(errors.Wrap(err, "Failed to get icon for "+desc))
}
}
}

View File

@ -79,47 +79,3 @@ func NewToggleButton(content text.Rich) *ToggleButton {
return &ToggleButton{*b, *l}
}
type ToggleButtonImage struct {
gtk.ToggleButton
Labeler
Label gtk.Label
Image gtk.Image
Box gtk.Box
}
var (
_ gtk.IWidget = (*ToggleButton)(nil)
_ cchat.LabelContainer = (*ToggleButton)(nil)
)
func NewToggleButtonImage(content text.Rich, iconName string) *ToggleButtonImage {
l := NewLabel(content)
l.Show()
var i *gtk.Image
if iconName != "" {
i, _ = gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_BUTTON)
} else {
i, _ = gtk.ImageNew()
}
i.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(i, false, false, 0)
box.PackStart(l, true, true, 5)
box.Show()
b, _ := gtk.ToggleButtonNew()
b.Add(box)
return &ToggleButtonImage{
ToggleButton: *b,
Labeler: l, // easy inheritance of methods
Label: l.Label,
Image: *i,
Box: *box,
}
}

View File

@ -2,12 +2,14 @@ package service
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const IconSize = 32
@ -115,14 +117,18 @@ type header struct {
}
func newHeader(svc cchat.Service) *header {
reveal := rich.NewToggleButtonImage(text.Rich{Content: svc.Name()}, "")
reveal := rich.NewToggleButtonImage(text.Rich{Content: svc.Name()})
reveal.Box.SetHAlign(gtk.ALIGN_START)
reveal.Image.SetPlaceholderIcon("folder-remote-symbolic", IconSize)
reveal.SetRelief(gtk.RELIEF_NONE)
reveal.SetMode(true)
reveal.Show()
// Set a custom icon.
primitives.SetImageIcon(&reveal.Image, "folder-remote-symbolic", IconSize)
if iconer, ok := svc.(cchat.Icon); ok {
if err := iconer.Icon(reveal); err != nil {
log.Error(errors.Wrap(err, "Error getting session logo"))
}
}
add, _ := gtk.ButtonNewFromIconName("list-add-symbolic", gtk.ICON_SIZE_BUTTON)
add.SetRelief(gtk.RELIEF_NONE)

View File

@ -13,6 +13,7 @@ import (
)
const ChildrenMargin = 24
const IconSize = 18
type Controller interface {
MessageRowSelected(*Row, cchat.ServerMessage)
@ -34,15 +35,12 @@ type Row struct {
}
func NewRow(parent breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller) *Row {
button := rich.NewToggleButtonImage(text.Rich{}, "")
button := rich.NewToggleButtonImage(text.Rich{})
button.Box.SetHAlign(gtk.ALIGN_START)
button.Image.SetSize(IconSize)
button.SetRelief(gtk.RELIEF_NONE)
button.Show()
if err := server.Name(button); err != nil {
log.Error(errors.Wrap(err, "Failed to get the server name"))
button.SetLabel(text.Rich{Content: "Unknown"})
}
button.Try(server, "server")
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.PackStart(button, false, false, 0)

View File

@ -2,14 +2,12 @@ package session
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const IconSize = 32
@ -38,23 +36,18 @@ func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Ro
}
row.Servers = server.NewChildren(row, ses, row)
row.Button = rich.NewToggleButtonImage(text.Rich{}, "")
row.Button = rich.NewToggleButtonImage(text.Rich{})
row.Button.Box.SetHAlign(gtk.ALIGN_START)
row.Button.Image.SetPlaceholderIcon("user-available-symbolic", IconSize)
row.Button.SetRelief(gtk.RELIEF_NONE)
row.Button.Show()
// On click, toggle reveal.
row.Button.Connect("clicked", func() {
revealed := !row.Servers.GetRevealChild()
row.Servers.SetRevealChild(revealed)
row.Button.SetActive(revealed)
})
primitives.SetImageIcon(&row.Button.Image, "user-available-symbolic", IconSize)
if err := ses.Name(row.Button); err != nil {
log.Error(errors.Wrap(err, "Failed to get the username"))
row.Button.SetLabel(text.Rich{Content: "Unknown"})
}
row.Button.Show()
row.Button.Try(ses, "session")
row.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
row.Box.SetMarginStart(server.ChildrenMargin)

View File

@ -1,7 +1,7 @@
package ui
import (
"github.com/diamondburned/cchat-gtk/internal/ui/message"
"github.com/diamondburned/cchat-gtk/internal/ui/messages"
"github.com/diamondburned/cchat-gtk/internal/ui/service"
"github.com/gotk3/gotk3/gtk"
)
@ -9,13 +9,13 @@ import (
type window struct {
*gtk.Box
Services *service.View
MessageView *message.View
MessageView *messages.View
}
func newWindow() *window {
services := service.NewView()
services.SetSizeRequest(LeftWidth, -1)
mesgview := message.NewView()
mesgview := messages.NewView()
separator, _ := gtk.SeparatorNew(gtk.ORIENTATION_VERTICAL)
separator.Show()