diff --git a/go.mod b/go.mod index 94741b5..1ce794f 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index f83441c..375547e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/gts/httputil/httputil.go b/internal/gts/httputil/httputil.go new file mode 100644 index 0000000..80c3733 --- /dev/null +++ b/internal/gts/httputil/httputil.go @@ -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 +} diff --git a/internal/gts/httputil/image.go b/internal/gts/httputil/image.go new file mode 100644 index 0000000..dc72d05 --- /dev/null +++ b/internal/gts/httputil/image.go @@ -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") +} diff --git a/internal/ui/message/compact/compact.go b/internal/ui/message/compact/compact.go deleted file mode 100644 index 6ca8a41..0000000 --- a/internal/ui/message/compact/compact.go +++ /dev/null @@ -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( - `%s`, - 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) - } - }) -} diff --git a/internal/ui/message/cozy/cozy.go b/internal/ui/message/cozy/cozy.go deleted file mode 100644 index 2284c72..0000000 --- a/internal/ui/message/cozy/cozy.go +++ /dev/null @@ -1 +0,0 @@ -package cozy diff --git a/internal/ui/message/autoscroll/autoscroll.go b/internal/ui/messages/autoscroll/autoscroll.go similarity index 82% rename from internal/ui/message/autoscroll/autoscroll.go rename to internal/ui/messages/autoscroll/autoscroll.go index 1efbdc4..adaafb1 100644 --- a/internal/ui/message/autoscroll/autoscroll.go +++ b/internal/ui/messages/autoscroll/autoscroll.go @@ -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 diff --git a/internal/ui/messages/container/compact/compact.go b/internal/ui/messages/container/compact/compact.go new file mode 100644 index 0000000..0bc4216 --- /dev/null +++ b/internal/ui/messages/container/compact/compact.go @@ -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) +} diff --git a/internal/ui/messages/container/compact/message.go b/internal/ui/messages/container/compact/message.go new file mode 100644 index 0000000..9bf48b9 --- /dev/null +++ b/internal/ui/messages/container/compact/message.go @@ -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) +} diff --git a/internal/ui/messages/container/container.go b/internal/ui/messages/container/container.go new file mode 100644 index 0000000..6c992e5 --- /dev/null +++ b/internal/ui/messages/container/container.go @@ -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) + } + }) +} diff --git a/internal/ui/messages/container/cozy/cozy.go b/internal/ui/messages/container/cozy/cozy.go new file mode 100644 index 0000000..05d6989 --- /dev/null +++ b/internal/ui/messages/container/cozy/cozy.go @@ -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 +} diff --git a/internal/ui/messages/container/cozy/message.go b/internal/ui/messages/container/cozy/message.go new file mode 100644 index 0000000..6038c6a --- /dev/null +++ b/internal/ui/messages/container/cozy/message.go @@ -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() diff --git a/internal/ui/message/input/input.go b/internal/ui/messages/input/input.go similarity index 100% rename from internal/ui/message/input/input.go rename to internal/ui/messages/input/input.go diff --git a/internal/ui/message/input/keydown.go b/internal/ui/messages/input/keydown.go similarity index 100% rename from internal/ui/message/input/keydown.go rename to internal/ui/messages/input/keydown.go diff --git a/internal/ui/message/input/send.go b/internal/ui/messages/input/send.go similarity index 100% rename from internal/ui/message/input/send.go rename to internal/ui/messages/input/send.go diff --git a/internal/ui/message/compact/message.go b/internal/ui/messages/message/message.go similarity index 51% rename from internal/ui/message/compact/message.go rename to internal/ui/messages/message/message.go index 0c034b1..c5772ef 100644 --- a/internal/ui/message/compact/message.go +++ b/internal/ui/messages/message/message.go @@ -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)) } diff --git a/internal/ui/messages/message/sending.go b/internal/ui/messages/message/sending.go new file mode 100644 index 0000000..38aa02c --- /dev/null +++ b/internal/ui/messages/message/sending.go @@ -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(`` + html.EscapeString(m.Content.GetLabel()) + ``) + m.Content.SetTooltipText(err.Error()) +} diff --git a/internal/ui/message/view.go b/internal/ui/messages/view.go similarity index 85% rename from internal/ui/message/view.go rename to internal/ui/messages/view.go index 9ee1cf5..56e5b29 100644 --- a/internal/ui/message/view.go +++ b/internal/ui/messages/view.go @@ -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) diff --git a/internal/ui/rich/image.go b/internal/ui/rich/image.go new file mode 100644 index 0000000..843831a --- /dev/null +++ b/internal/ui/rich/image.go @@ -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)) + } + } +} diff --git a/internal/ui/rich/rich.go b/internal/ui/rich/rich.go index f49decc..8178d14 100644 --- a/internal/ui/rich/rich.go +++ b/internal/ui/rich/rich.go @@ -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, - } -} diff --git a/internal/ui/service/service.go b/internal/ui/service/service.go index 4f4f27b..5384e40 100644 --- a/internal/ui/service/service.go +++ b/internal/ui/service/service.go @@ -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) diff --git a/internal/ui/service/session/server/server.go b/internal/ui/service/session/server/server.go index 85bfcc7..2637788 100644 --- a/internal/ui/service/session/server/server.go +++ b/internal/ui/service/session/server/server.go @@ -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) diff --git a/internal/ui/service/session/session.go b/internal/ui/service/session/session.go index 49e94e9..ca21153 100644 --- a/internal/ui/service/session/session.go +++ b/internal/ui/service/session/session.go @@ -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) diff --git a/internal/ui/window.go b/internal/ui/window.go index e4b4ef4..798caa7 100644 --- a/internal/ui/window.go +++ b/internal/ui/window.go @@ -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()