mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-05-23 15:41:08 +00:00
Session restore and icons added
This commit is contained in:
parent
2d628fbbb3
commit
43aa4dcef3
2
go.mod
2
go.mod
|
@ -4,6 +4,8 @@ go 1.14
|
||||||
|
|
||||||
replace github.com/diamondburned/cchat-mock => ../cchat-mock/
|
replace github.com/diamondburned/cchat-mock => ../cchat-mock/
|
||||||
|
|
||||||
|
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200606223630-b0c33ec7b10a
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Xuanwo/go-locale v0.2.0
|
github.com/Xuanwo/go-locale v0.2.0
|
||||||
github.com/diamondburned/cchat v0.0.15
|
github.com/diamondburned/cchat v0.0.15
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -9,6 +9,8 @@ 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/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 h1:1o4OX8zw/CdSv3Idaylz7vjHVOZKEi/xkg8BpEvtsHY=
|
||||||
github.com/diamondburned/cchat v0.0.15/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat v0.0.15/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20200606223630-b0c33ec7b10a h1:pH6WLWVhzzvpXjGwPQbPhhp1g0ZMLZFS5S8zLoRGYRg=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20200606223630-b0c33ec7b10a/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200606035324-63abbc0fdea6 h1:APALM1hskCByjOVW9CoUwjg0TIJgKZ62dgFr/9soqss=
|
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/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 h1:U/5Sq2nJQ0XDyks+8ATghtHSuquIGq7JYrqSrvtR2dg=
|
||||||
|
|
|
@ -2,9 +2,9 @@ package gts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/glib"
|
"github.com/gotk3/gotk3/glib"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
@ -12,7 +12,6 @@ import (
|
||||||
const AppID = "com.github.diamondburned.cchat-gtk"
|
const AppID = "com.github.diamondburned.cchat-gtk"
|
||||||
|
|
||||||
var Args = append([]string{}, os.Args...)
|
var Args = append([]string{}, os.Args...)
|
||||||
var recvPool *sync.Pool
|
|
||||||
|
|
||||||
var App struct {
|
var App struct {
|
||||||
*gtk.Application
|
*gtk.Application
|
||||||
|
@ -22,13 +21,6 @@ var App struct {
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gtk.Init(&Args)
|
gtk.Init(&Args)
|
||||||
|
|
||||||
recvPool = &sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
return make(chan struct{})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Application, _ = gtk.ApplicationNew(AppID, 0)
|
App.Application, _ = gtk.ApplicationNew(AppID, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,8 +90,7 @@ func ExecAsync(fn func()) {
|
||||||
// ExecSync executes the function asynchronously, but returns a channel that
|
// ExecSync executes the function asynchronously, but returns a channel that
|
||||||
// indicates when the job is done.
|
// indicates when the job is done.
|
||||||
func ExecSync(fn func()) <-chan struct{} {
|
func ExecSync(fn func()) <-chan struct{} {
|
||||||
ch := recvPool.Get().(chan struct{})
|
var ch = make(chan struct{})
|
||||||
defer recvPool.Put(ch)
|
|
||||||
|
|
||||||
glib.IdleAdd(func() {
|
glib.IdleAdd(func() {
|
||||||
fn()
|
fn()
|
||||||
|
@ -108,3 +99,8 @@ func ExecSync(fn func()) <-chan struct{} {
|
||||||
|
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EventIsRightClick(ev *gdk.Event) bool {
|
||||||
|
keyev := gdk.EventButtonNewFromEvent(ev)
|
||||||
|
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
|
||||||
|
}
|
||||||
|
|
|
@ -8,16 +8,68 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/imgutil"
|
"github.com/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AsyncImage loads an image. This method uses the cache.
|
type ImageContainer interface {
|
||||||
func AsyncImage(img *gtk.Image, url string, procs ...imgutil.Processor) {
|
SetFromPixbuf(*gdk.Pixbuf)
|
||||||
go asyncImage(img, url, procs...)
|
SetFromAnimation(*gdk.PixbufAnimation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func asyncImage(img *gtk.Image, url string, procs ...imgutil.Processor) {
|
type ImageContainerSizer interface {
|
||||||
|
ImageContainer
|
||||||
|
SetSizeRequest(w, h int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsyncImage loads an image. This method uses the cache.
|
||||||
|
func AsyncImage(img ImageContainer, url string, procs ...imgutil.Processor) {
|
||||||
|
go syncImageFn(img, url, procs, func(l *gdk.PixbufLoader, gif bool) {
|
||||||
|
l.Connect("area-prepared", areaPreparedFn(img, gif))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsyncImageSized resizes using GdkPixbuf. This method does not use the cache.
|
||||||
|
func AsyncImageSized(img ImageContainerSizer, url string, w, h int, procs ...imgutil.Processor) {
|
||||||
|
go syncImageFn(img, url, procs, func(l *gdk.PixbufLoader, gif bool) {
|
||||||
|
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
|
||||||
|
w, h = imgutil.MaxSize(imgW, imgH, w, h)
|
||||||
|
if w != imgW || h != imgH {
|
||||||
|
l.SetSize(w, h)
|
||||||
|
img.SetSizeRequest(w, h)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
l.Connect("area-prepared", areaPreparedFn(img, gif))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func areaPreparedFn(img ImageContainer, gif bool) func(l *gdk.PixbufLoader) {
|
||||||
|
return func(l *gdk.PixbufLoader) {
|
||||||
|
if !gif {
|
||||||
|
p, err := l.GetPixbuf()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "Failed to get pixbuf"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gts.ExecAsync(func() { img.SetFromPixbuf(p) })
|
||||||
|
} else {
|
||||||
|
p, err := l.GetAnimation()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "Failed to get animation"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gts.ExecAsync(func() { img.SetFromAnimation(p) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncImageFn(
|
||||||
|
img ImageContainer,
|
||||||
|
url string,
|
||||||
|
procs []imgutil.Processor,
|
||||||
|
middle func(l *gdk.PixbufLoader, gif bool),
|
||||||
|
) {
|
||||||
|
|
||||||
r, err := get(url, true)
|
r, err := get(url, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
|
@ -36,24 +88,8 @@ func asyncImage(img *gtk.Image, url string, procs ...imgutil.Processor) {
|
||||||
// This is a very important signal, so we must do it synchronously. Gotk3's
|
// This is a very important signal, so we must do it synchronously. Gotk3's
|
||||||
// callback implementation requires all connects to be synchronous to a
|
// callback implementation requires all connects to be synchronous to a
|
||||||
// certain thread.
|
// certain thread.
|
||||||
gts.ExecSync(func() {
|
<-gts.ExecSync(func() {
|
||||||
l.Connect("area-prepared", func() {
|
middle(l, gif)
|
||||||
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 we have processors, then write directly in there.
|
||||||
|
@ -77,9 +113,3 @@ func asyncImage(img *gtk.Image, url string, procs ...imgutil.Processor) {
|
||||||
log.Error(errors.Wrap(err, "Failed to close pixbuf"))
|
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")
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,14 +5,12 @@ import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/zalando/go-keyring"
|
"github.com/zalando/go-keyring"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getThenDestroy(service string, v interface{}) error {
|
func get(service string, v interface{}) error {
|
||||||
s, err := keyring.Get("cchat-gtk", service)
|
s, err := keyring.Get("cchat-gtk", service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -33,57 +31,24 @@ func set(service string, v interface{}) error {
|
||||||
return keyring.Set("cchat-gtk", service, b.String())
|
return keyring.Set("cchat-gtk", service, b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveSessions(service cchat.Service, sessions []cchat.Session) (saveErrs []error) {
|
type Session struct {
|
||||||
var sessionData = make([]map[string]string, 0, len(sessions))
|
ID string
|
||||||
|
Name string
|
||||||
|
Data map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
for _, session := range sessions {
|
func SaveSessions(serviceName string, sessions []Session) {
|
||||||
sv, ok := session.(cchat.SessionSaver)
|
if err := set(serviceName, sessions); err != nil {
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
d, err := sv.Save()
|
|
||||||
if err != nil {
|
|
||||||
saveErrs = append(saveErrs, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionData = append(sessionData, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := set(service.Name(), sessionData); err != nil {
|
|
||||||
log.Warn(errors.Wrap(err, "Error saving session"))
|
log.Warn(errors.Wrap(err, "Error saving session"))
|
||||||
saveErrs = append(saveErrs, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreSessions restores all sessions of the service asynchronously, then
|
// RestoreSessions restores all sessions of the service asynchronously, then
|
||||||
// calls the auth callback inside the Gtk main thread.
|
// calls the auth callback inside the Gtk main thread.
|
||||||
func RestoreSessions(service cchat.Service, auth func(cchat.Session)) {
|
func RestoreSessions(serviceName string) (sessions []Session) {
|
||||||
// If the service doesn't support restoring, treat it as a non-error.
|
|
||||||
restorer, ok := service.(cchat.SessionRestorer)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var sessionData []map[string]string
|
|
||||||
|
|
||||||
// Ignore the error, it's not important.
|
// Ignore the error, it's not important.
|
||||||
if err := getThenDestroy(service.Name(), &sessionData); err != nil {
|
if err := get(serviceName, &sessions); err != nil {
|
||||||
log.Warn(err)
|
log.Warn(err)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, data := range sessionData {
|
|
||||||
gts.Async(func() (func(), error) {
|
|
||||||
s, err := restorer.RestoreSession(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Failed to restore")
|
|
||||||
}
|
|
||||||
|
|
||||||
return func() { auth(s) }, nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,17 @@ type Container struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer() *Container {
|
func NewContainer() *Container {
|
||||||
c := &Container{}
|
return &Container{
|
||||||
c.GridContainer = container.NewGridContainer(c)
|
GridContainer: container.NewGridContainer(constructor{}),
|
||||||
return c
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
|
type constructor struct{}
|
||||||
|
|
||||||
|
func (constructor) NewMessage(msg cchat.MessageCreate) container.GridMessage {
|
||||||
return NewMessage(msg)
|
return NewMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
|
func (constructor) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
|
||||||
return NewPresendMessage(msg)
|
return NewPresendMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,17 +9,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type PresendMessage struct {
|
type PresendMessage struct {
|
||||||
*message.GenericPresendContainer
|
message.PresendContainer
|
||||||
|
Message
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
|
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
|
||||||
return PresendMessage{
|
var msgc = message.NewPresendContainer(msg)
|
||||||
GenericPresendContainer: message.NewPresendContainer(msg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p PresendMessage) Attach(grid *gtk.Grid, row int) {
|
return PresendMessage{
|
||||||
attachGenericContainer(p.GenericContainer, grid, row)
|
PresendContainer: msgc,
|
||||||
|
Message: Message{msgc.GenericContainer},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
|
@ -29,15 +29,13 @@ type Message struct {
|
||||||
var _ container.GridMessage = (*Message)(nil)
|
var _ container.GridMessage = (*Message)(nil)
|
||||||
|
|
||||||
func NewMessage(msg cchat.MessageCreate) Message {
|
func NewMessage(msg cchat.MessageCreate) Message {
|
||||||
return Message{
|
msgc := message.NewContainer(msg)
|
||||||
GenericContainer: message.NewContainer(msg),
|
message.FillContainer(msgc, msg)
|
||||||
}
|
return Message{msgc}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmptyMessage() Message {
|
func NewEmptyMessage() Message {
|
||||||
return Message{
|
return Message{message.NewEmptyContainer()}
|
||||||
GenericContainer: message.NewEmptyContainer(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fix a bug here related to new messages overlapping
|
// TODO: fix a bug here related to new messages overlapping
|
||||||
|
@ -46,7 +44,5 @@ func (m Message) Attach(grid *gtk.Grid, row int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachGenericContainer(m *message.GenericContainer, grid *gtk.Grid, row int) {
|
func attachGenericContainer(m *message.GenericContainer, grid *gtk.Grid, row int) {
|
||||||
grid.Attach(m.Timestamp, 0, row, 1, 1)
|
container.AttachRow(grid, row, m.Timestamp, m.Username, m.Content)
|
||||||
grid.Attach(m.Username, 1, row, 1, 1)
|
|
||||||
grid.Attach(m.Content, 2, row, 1, 1)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,13 +21,6 @@ type PresendGridMessage interface {
|
||||||
message.PresendContainer
|
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
|
// Constructor is an interface for making custom message implementations which
|
||||||
// allows GridContainer to generically work with.
|
// allows GridContainer to generically work with.
|
||||||
type Constructor interface {
|
type Constructor interface {
|
||||||
|
@ -47,6 +40,14 @@ type Container interface {
|
||||||
PresendMessage(input.PresendMessage) (done func(sendError error))
|
PresendMessage(input.PresendMessage) (done func(sendError error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AttachRow(grid *gtk.Grid, row int, widgets ...gtk.IWidget) {
|
||||||
|
for i, w := range widgets {
|
||||||
|
grid.Attach(w, i, row, 1, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColumnSpacing = 10
|
||||||
|
|
||||||
// GridContainer is an implementation of Container, which allows flexible
|
// GridContainer is an implementation of Container, which allows flexible
|
||||||
// message grids.
|
// message grids.
|
||||||
type GridContainer struct {
|
type GridContainer struct {
|
||||||
|
@ -55,8 +56,15 @@ type GridContainer struct {
|
||||||
|
|
||||||
construct Constructor
|
construct Constructor
|
||||||
|
|
||||||
messages map[string]*gridMessage
|
messages []*gridMessage // sync w/ grid rows
|
||||||
nonceMsgs map[string]*gridMessage
|
messageIDs map[string]int
|
||||||
|
nonceMsgs map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
// gridMessage w/ required internals
|
||||||
|
type gridMessage struct {
|
||||||
|
GridMessage
|
||||||
|
presend message.PresendContainer // this shouldn't be here but i'm lazy
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -66,7 +74,7 @@ var (
|
||||||
|
|
||||||
func NewGridContainer(constr Constructor) *GridContainer {
|
func NewGridContainer(constr Constructor) *GridContainer {
|
||||||
grid, _ := gtk.GridNew()
|
grid, _ := gtk.GridNew()
|
||||||
grid.SetColumnSpacing(10)
|
grid.SetColumnSpacing(ColumnSpacing)
|
||||||
grid.SetRowSpacing(5)
|
grid.SetRowSpacing(5)
|
||||||
grid.SetMarginStart(5)
|
grid.SetMarginStart(5)
|
||||||
grid.SetMarginEnd(5)
|
grid.SetMarginEnd(5)
|
||||||
|
@ -82,42 +90,42 @@ func NewGridContainer(constr Constructor) *GridContainer {
|
||||||
ScrolledWindow: sw,
|
ScrolledWindow: sw,
|
||||||
Main: grid,
|
Main: grid,
|
||||||
construct: constr,
|
construct: constr,
|
||||||
messages: map[string]*gridMessage{},
|
messageIDs: map[string]int{},
|
||||||
nonceMsgs: map[string]*gridMessage{},
|
nonceMsgs: map[string]int{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &container
|
return &container
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridContainer) Reset() {
|
func (c *GridContainer) Reset() {
|
||||||
// does this actually work?
|
c.Main.GetChildren().Foreach(func(v interface{}) {
|
||||||
var rows = c.len()
|
// Unsafe assertion ftw.
|
||||||
for i := 0; i < rows; i++ {
|
c.Main.Remove(v.(gtk.IWidget))
|
||||||
c.Main.RemoveRow(i)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
c.messages = map[string]*gridMessage{}
|
c.messages = nil
|
||||||
c.nonceMsgs = map[string]*gridMessage{}
|
c.messageIDs = map[string]int{}
|
||||||
|
c.nonceMsgs = map[string]int{}
|
||||||
|
|
||||||
c.ScrolledWindow.Bottomed = true
|
c.ScrolledWindow.Bottomed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridContainer) len() int {
|
|
||||||
return len(c.messages) + len(c.nonceMsgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PresendMessage is not thread-safe.
|
// PresendMessage is not thread-safe.
|
||||||
func (c *GridContainer) PresendMessage(msg input.PresendMessage) func(error) {
|
func (c *GridContainer) PresendMessage(msg input.PresendMessage) func(error) {
|
||||||
presend := c.construct.NewPresendMessage(msg)
|
presend := c.construct.NewPresendMessage(msg)
|
||||||
|
|
||||||
msgc := gridMessage{
|
msgc := &gridMessage{
|
||||||
GridMessage: presend,
|
GridMessage: presend,
|
||||||
presend: presend,
|
presend: presend,
|
||||||
index: c.len(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.nonceMsgs[presend.Nonce()] = &msgc
|
// Grab index before appending, as that'll be where the added message is.
|
||||||
msgc.Attach(c.Main, msgc.index)
|
index := len(c.messages)
|
||||||
|
|
||||||
|
c.messages = append(c.messages, msgc)
|
||||||
|
|
||||||
|
c.nonceMsgs[presend.Nonce()] = index
|
||||||
|
msgc.Attach(c.Main, index)
|
||||||
|
|
||||||
return func(err error) {
|
return func(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -127,19 +135,31 @@ func (c *GridContainer) PresendMessage(msg input.PresendMessage) func(error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindMessage is not thread-safe. This exists for backwards compatibility.
|
// FindMessage iterates backwards and returns the message if isMessage() returns
|
||||||
func (c *GridContainer) FindMessage(msg cchat.MessageHeader) GridMessage {
|
// true on that message.
|
||||||
if m := c.findMessage(msg); m != nil {
|
func (c *GridContainer) FindMessage(isMessage func(msg GridMessage) bool) GridMessage {
|
||||||
|
for i := len(c.messages) - 1; i >= 0; i-- {
|
||||||
|
if msg := c.messages[i].GridMessage; isMessage(msg) {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message finds the message state in the container. It is not thread-safe. This
|
||||||
|
// exists for backwards compatibility.
|
||||||
|
func (c *GridContainer) Message(msg cchat.MessageHeader) GridMessage {
|
||||||
|
if m := c.message(msg); m != nil {
|
||||||
return m.GridMessage
|
return m.GridMessage
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridContainer) findMessage(msg cchat.MessageHeader) *gridMessage {
|
func (c *GridContainer) message(msg cchat.MessageHeader) *gridMessage {
|
||||||
// Search using the ID first.
|
// Search using the ID first.
|
||||||
m, ok := c.messages[msg.ID()]
|
i, ok := c.messageIDs[msg.ID()]
|
||||||
if ok {
|
if ok {
|
||||||
return m
|
return c.messages[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this an existing message?
|
// Is this an existing message?
|
||||||
|
@ -147,11 +167,14 @@ func (c *GridContainer) findMessage(msg cchat.MessageHeader) *gridMessage {
|
||||||
var nonce = noncer.Nonce()
|
var nonce = noncer.Nonce()
|
||||||
|
|
||||||
// Things in this map are guaranteed to have presend != nil.
|
// Things in this map are guaranteed to have presend != nil.
|
||||||
m, ok := c.nonceMsgs[nonce]
|
i, ok := c.nonceMsgs[nonce]
|
||||||
if ok {
|
if ok {
|
||||||
// Move the message outside nonceMsgs.
|
// Move the message outside nonceMsgs and into messageIDs.
|
||||||
delete(c.nonceMsgs, nonce)
|
delete(c.nonceMsgs, nonce)
|
||||||
c.messages[msg.ID()] = m
|
c.messageIDs[msg.ID()] = i
|
||||||
|
|
||||||
|
// Get the message pointer.
|
||||||
|
m := c.messages[i]
|
||||||
|
|
||||||
// Set the right ID.
|
// Set the right ID.
|
||||||
m.presend.SetID(msg.ID())
|
m.presend.SetID(msg.ID())
|
||||||
|
@ -168,27 +191,31 @@ func (c *GridContainer) findMessage(msg cchat.MessageHeader) *gridMessage {
|
||||||
|
|
||||||
func (c *GridContainer) CreateMessage(msg cchat.MessageCreate) {
|
func (c *GridContainer) CreateMessage(msg cchat.MessageCreate) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
// Attempt update before insert (aka upsert).
|
// Attempt to update before insertion (aka upsert).
|
||||||
if msgc := c.FindMessage(msg); msgc != nil {
|
if msgc := c.Message(msg); msgc != nil {
|
||||||
msgc.UpdateAuthor(msg.Author())
|
msgc.UpdateAuthor(msg.Author())
|
||||||
msgc.UpdateContent(msg.Content())
|
msgc.UpdateContent(msg.Content())
|
||||||
msgc.UpdateTimestamp(msg.Time())
|
msgc.UpdateTimestamp(msg.Time())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msgc := gridMessage{
|
msgc := &gridMessage{
|
||||||
GridMessage: c.construct.NewMessage(msg),
|
GridMessage: c.construct.NewMessage(msg),
|
||||||
index: c.len(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.messages[msgc.ID()] = &msgc
|
// Grab index before appending, as that'll be where the added message is.
|
||||||
msgc.Attach(c.Main, msgc.index)
|
index := len(c.messages)
|
||||||
|
|
||||||
|
c.messages = append(c.messages, msgc)
|
||||||
|
|
||||||
|
c.messageIDs[msgc.ID()] = index
|
||||||
|
msgc.Attach(c.Main, index)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
|
func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
if msgc := c.FindMessage(msg); msgc != nil {
|
if msgc := c.Message(msg); msgc != nil {
|
||||||
if author := msg.Author(); author != nil {
|
if author := msg.Author(); author != nil {
|
||||||
msgc.UpdateAuthor(author)
|
msgc.UpdateAuthor(author)
|
||||||
}
|
}
|
||||||
|
@ -202,9 +229,13 @@ func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
|
||||||
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
|
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
// TODO: add nonce check.
|
// TODO: add nonce check.
|
||||||
if m, ok := c.messages[msg.ID()]; ok {
|
if i, ok := c.messageIDs[msg.ID()]; ok {
|
||||||
delete(c.messages, msg.ID())
|
// Remove off the slice.
|
||||||
c.Main.RemoveRow(m.index)
|
c.messages = append(c.messages[:i], c.messages[i+1:]...)
|
||||||
|
|
||||||
|
// Remove off the map.
|
||||||
|
delete(c.messageIDs, msg.ID())
|
||||||
|
c.Main.RemoveRow(i)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,62 @@
|
||||||
package cozy
|
package cozy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/autoscroll"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AvatarPixbufCopier interface {
|
||||||
|
CopyAvatarPixbuf(httputil.ImageContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AvatarSize = 40
|
||||||
|
AvatarMargin = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
*autoscroll.ScrolledWindow
|
*container.GridContainer
|
||||||
main *gtk.Grid
|
}
|
||||||
messages map[string]Message
|
|
||||||
nonceMsgs map[string]Message
|
func NewContainer() *Container {
|
||||||
|
c := &Container{}
|
||||||
|
c.GridContainer = container.NewGridContainer(c)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
|
||||||
|
var newmsg = NewFullMessage(msg)
|
||||||
|
|
||||||
|
// Try and reuse an existing avatar.
|
||||||
|
if author := msg.Author(); !c.reuseAvatar(author.ID(), newmsg.Avatar) {
|
||||||
|
// Fetch a new avatar if we can't reuse the old one.
|
||||||
|
newmsg.updateAuthorAvatar(author)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
|
||||||
|
var presend = NewFullSendingMessage(msg)
|
||||||
|
c.reuseAvatar(msg.AuthorID(), presend.Avatar)
|
||||||
|
return presend
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) reuseAvatar(authorID string, img httputil.ImageContainer) (reused bool) {
|
||||||
|
// Search the old author if we have any.
|
||||||
|
msgc := c.FindMessage(func(msgc container.GridMessage) bool {
|
||||||
|
return msgc.AuthorID() == authorID
|
||||||
|
})
|
||||||
|
|
||||||
|
// Is this a message that we can work with? We have to assert to
|
||||||
|
// FullSendingMessage because that's where our messages are.
|
||||||
|
copier, ok := msgc.(AvatarPixbufCopier)
|
||||||
|
if ok {
|
||||||
|
// Borrow the avatar URL.
|
||||||
|
copier.CopyAvatarPixbuf(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
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()
|
|
11
internal/ui/messages/container/cozy/message_compact.go
Normal file
11
internal/ui/messages/container/cozy/message_compact.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package cozy
|
||||||
|
|
||||||
|
// CompactMessage is a message that follows after FullMessage. It does not show
|
||||||
|
// the header, and the avatar is invisible.
|
||||||
|
type CompactMessage struct {
|
||||||
|
// Essentially, CompactMessage is just a full message with some things
|
||||||
|
// hidden. Its Avatar and Timestamp will still be updated. This is a
|
||||||
|
// trade-off between performance, efficiency and code length.
|
||||||
|
|
||||||
|
*FullMessage
|
||||||
|
}
|
116
internal/ui/messages/container/cozy/message_full.go
Normal file
116
internal/ui/messages/container/cozy/message_full.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package cozy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
|
"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/diamondburned/imgutil"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FullMessage struct {
|
||||||
|
*message.GenericContainer
|
||||||
|
|
||||||
|
// Grid widgets.
|
||||||
|
Avatar *gtk.Image
|
||||||
|
MainBox *gtk.Box // wraps header and content
|
||||||
|
|
||||||
|
// Header wraps author and timestamp.
|
||||||
|
HeaderBox *gtk.Box
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ AvatarPixbufCopier = (*FullMessage)(nil)
|
||||||
|
_ message.Container = (*FullMessage)(nil)
|
||||||
|
_ container.GridMessage = (*FullMessage)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
|
||||||
|
msgc := WrapFullMessage(message.NewContainer(msg))
|
||||||
|
// Don't update the avatar.
|
||||||
|
msgc.UpdateContent(msg.Content())
|
||||||
|
msgc.UpdateAuthorName(msg.Author().Name())
|
||||||
|
msgc.UpdateTimestamp(msg.Time())
|
||||||
|
return msgc
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
||||||
|
avatar, _ := gtk.ImageNew()
|
||||||
|
avatar.SetSizeRequest(AvatarSize, AvatarSize)
|
||||||
|
avatar.SetVAlign(gtk.ALIGN_START)
|
||||||
|
avatar.SetMarginStart(container.ColumnSpacing * 2)
|
||||||
|
avatar.Show()
|
||||||
|
|
||||||
|
header, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
header.PackStart(gc.Username, false, false, 0)
|
||||||
|
header.PackStart(gc.Timestamp, false, false, 7) // padding
|
||||||
|
header.Show()
|
||||||
|
|
||||||
|
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
main.PackStart(header, false, false, 0)
|
||||||
|
main.PackStart(gc.Content, false, false, 2)
|
||||||
|
main.SetMarginBottom(2)
|
||||||
|
main.SetMarginEnd(container.ColumnSpacing * 2)
|
||||||
|
main.Show()
|
||||||
|
|
||||||
|
return &FullMessage{
|
||||||
|
GenericContainer: gc,
|
||||||
|
Avatar: avatar,
|
||||||
|
MainBox: main,
|
||||||
|
HeaderBox: header,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FullMessage) UpdateAuthor(author cchat.MessageAuthor) {
|
||||||
|
// Call the parent's method to update the labels.
|
||||||
|
m.GenericContainer.UpdateAuthor(author)
|
||||||
|
m.updateAuthorAvatar(author)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FullMessage) updateAuthorAvatar(author cchat.MessageAuthor) {
|
||||||
|
// If the author has an avatar:
|
||||||
|
if avatarer, ok := author.(cchat.MessageAuthorAvatar); ok {
|
||||||
|
// Download the avatar asynchronously.
|
||||||
|
httputil.AsyncImageSized(
|
||||||
|
m.Avatar,
|
||||||
|
avatarer.Avatar(),
|
||||||
|
AvatarSize, AvatarSize,
|
||||||
|
imgutil.Round(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FullMessage) CopyAvatarPixbuf(dst httputil.ImageContainer) {
|
||||||
|
switch m.Avatar.GetStorageType() {
|
||||||
|
case gtk.IMAGE_PIXBUF:
|
||||||
|
dst.SetFromPixbuf(m.Avatar.GetPixbuf())
|
||||||
|
case gtk.IMAGE_ANIMATION:
|
||||||
|
dst.SetFromAnimation(m.Avatar.GetAnimation())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FullMessage) Attach(grid *gtk.Grid, row int) {
|
||||||
|
container.AttachRow(grid, row, m.Avatar, m.MainBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FullSendingMessage struct {
|
||||||
|
message.PresendContainer
|
||||||
|
FullMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ AvatarPixbufCopier = (*FullSendingMessage)(nil)
|
||||||
|
_ message.Container = (*FullSendingMessage)(nil)
|
||||||
|
_ container.GridMessage = (*FullSendingMessage)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage {
|
||||||
|
var msgc = message.NewPresendContainer(msg)
|
||||||
|
|
||||||
|
return &FullSendingMessage{
|
||||||
|
PresendContainer: msgc,
|
||||||
|
FullMessage: *WrapFullMessage(msgc.GenericContainer),
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,51 +4,10 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type usernameContainer struct {
|
|
||||||
*gtk.Revealer
|
|
||||||
label *rich.Label
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUsernameContainer() *usernameContainer {
|
|
||||||
label := rich.NewLabel(text.Rich{})
|
|
||||||
label.SetMaxWidthChars(35)
|
|
||||||
label.SetVAlign(gtk.ALIGN_START)
|
|
||||||
label.SetMarginTop(inputmargin)
|
|
||||||
label.SetMarginBottom(inputmargin)
|
|
||||||
label.SetMarginStart(10)
|
|
||||||
label.SetMarginEnd(10)
|
|
||||||
label.Show()
|
|
||||||
|
|
||||||
rev, _ := gtk.RevealerNew()
|
|
||||||
rev.SetRevealChild(false)
|
|
||||||
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
|
||||||
rev.SetTransitionDuration(50)
|
|
||||||
rev.Add(label)
|
|
||||||
|
|
||||||
return &usernameContainer{rev, label}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLabel is not thread-safe.
|
|
||||||
func (u *usernameContainer) GetLabel() text.Rich {
|
|
||||||
return u.label.GetLabel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLabel is thread-safe.
|
|
||||||
func (u *usernameContainer) SetLabel(content text.Rich) {
|
|
||||||
gts.ExecAsync(func() {
|
|
||||||
u.label.SetLabelUnsafe(content)
|
|
||||||
|
|
||||||
// Reveal if the name is not empty.
|
|
||||||
u.SetRevealChild(!u.label.GetLabel().Empty())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
username *usernameContainer
|
username *usernameContainer
|
||||||
|
@ -117,18 +76,8 @@ func NewField(ctrl Controller) *Field {
|
||||||
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
||||||
f.UserID = session.ID()
|
f.UserID = session.ID()
|
||||||
|
|
||||||
// Does sender (aka Server) implement ServerNickname?
|
// Update the left username container in the input.
|
||||||
var err error
|
f.username.Update(session, sender)
|
||||||
if nicknamer, ok := sender.(cchat.ServerNickname); ok {
|
|
||||||
err = errors.Wrap(nicknamer.Nickname(f.username), "Failed to get nickname")
|
|
||||||
} else {
|
|
||||||
err = errors.Wrap(session.Name(f.username), "Failed to get username")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do a bit of trivial error handling.
|
|
||||||
if err != nil {
|
|
||||||
log.Warn(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the sender.
|
// Set the sender.
|
||||||
f.sender = sender
|
f.sender = sender
|
||||||
|
|
115
internal/ui/messages/input/username.go
Normal file
115
internal/ui/messages/input/username.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package input
|
||||||
|
|
||||||
|
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/rich"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/imgutil"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AvatarSize = 20
|
||||||
|
|
||||||
|
type usernameContainer struct {
|
||||||
|
*gtk.Revealer
|
||||||
|
|
||||||
|
main *gtk.Box
|
||||||
|
|
||||||
|
avatar *rich.Icon
|
||||||
|
label *rich.Label
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.LabelContainer = (*usernameContainer)(nil)
|
||||||
|
_ cchat.IconContainer = (*usernameContainer)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func newUsernameContainer() *usernameContainer {
|
||||||
|
avatar := rich.NewIcon(AvatarSize, imgutil.Round(true))
|
||||||
|
avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
|
||||||
|
avatar.Show()
|
||||||
|
|
||||||
|
label := rich.NewLabel(text.Rich{})
|
||||||
|
label.SetMaxWidthChars(35)
|
||||||
|
label.Show()
|
||||||
|
|
||||||
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
|
||||||
|
box.PackStart(avatar, false, false, 0)
|
||||||
|
box.PackStart(label, false, false, 0)
|
||||||
|
box.SetMarginStart(10)
|
||||||
|
box.SetMarginEnd(10)
|
||||||
|
box.SetMarginTop(inputmargin)
|
||||||
|
box.SetMarginBottom(inputmargin)
|
||||||
|
box.SetVAlign(gtk.ALIGN_START)
|
||||||
|
box.Show()
|
||||||
|
|
||||||
|
rev, _ := gtk.RevealerNew()
|
||||||
|
rev.SetRevealChild(false)
|
||||||
|
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
||||||
|
rev.SetTransitionDuration(50)
|
||||||
|
rev.Add(box)
|
||||||
|
|
||||||
|
return &usernameContainer{
|
||||||
|
Revealer: rev,
|
||||||
|
main: box,
|
||||||
|
avatar: avatar,
|
||||||
|
label: label,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update is not thread-safe.
|
||||||
|
func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMessageSender) {
|
||||||
|
// Does sender (aka Server) implement ServerNickname? If not, we fallback to
|
||||||
|
// the username inside session.
|
||||||
|
var err error
|
||||||
|
if nicknamer, ok := sender.(cchat.ServerNickname); ok {
|
||||||
|
err = errors.Wrap(nicknamer.Nickname(u), "Failed to get nickname")
|
||||||
|
} else {
|
||||||
|
err = errors.Wrap(session.Name(u), "Failed to get username")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a bit of trivial error handling.
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does session implement an icon? Update if so.
|
||||||
|
if iconer, ok := session.(cchat.Icon); ok {
|
||||||
|
err = iconer.Icon(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(errors.Wrap(err, "Failed to get icon"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLabel is not thread-safe.
|
||||||
|
func (u *usernameContainer) GetLabel() text.Rich {
|
||||||
|
return u.label.GetLabel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLabel is thread-safe.
|
||||||
|
func (u *usernameContainer) SetLabel(content text.Rich) {
|
||||||
|
gts.ExecAsync(func() {
|
||||||
|
u.label.SetLabelUnsafe(content)
|
||||||
|
|
||||||
|
// Reveal if the name is not empty.
|
||||||
|
u.SetRevealChild(!u.label.GetLabel().Empty())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIcon is thread-safe.
|
||||||
|
func (u *usernameContainer) SetIcon(url string) {
|
||||||
|
gts.ExecAsync(func() {
|
||||||
|
u.avatar.SetIconUnsafe(url)
|
||||||
|
|
||||||
|
// Reveal if the icon URL is not empty. We don't touch anything if the
|
||||||
|
// URL is empty, as the name might not be.
|
||||||
|
if url != "" {
|
||||||
|
u.SetRevealChild(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -22,6 +22,12 @@ type Container interface {
|
||||||
UpdateTimestamp(time.Time)
|
UpdateTimestamp(time.Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FillContainer(c Container, msg cchat.MessageCreate) {
|
||||||
|
c.UpdateAuthor(msg.Author())
|
||||||
|
c.UpdateContent(msg.Content())
|
||||||
|
c.UpdateTimestamp(msg.Time())
|
||||||
|
}
|
||||||
|
|
||||||
// GenericContainer provides a single generic message container for subpackages
|
// GenericContainer provides a single generic message container for subpackages
|
||||||
// to use.
|
// to use.
|
||||||
type GenericContainer struct {
|
type GenericContainer struct {
|
||||||
|
@ -36,12 +42,12 @@ type GenericContainer struct {
|
||||||
|
|
||||||
var _ Container = (*GenericContainer)(nil)
|
var _ Container = (*GenericContainer)(nil)
|
||||||
|
|
||||||
|
// NewContainer creates a new message container with the given ID and nonce. It
|
||||||
|
// does not update the widgets, so FillContainer should be called afterwards.
|
||||||
func NewContainer(msg cchat.MessageCreate) *GenericContainer {
|
func NewContainer(msg cchat.MessageCreate) *GenericContainer {
|
||||||
c := NewEmptyContainer()
|
c := NewEmptyContainer()
|
||||||
c.id = msg.ID()
|
c.id = msg.ID()
|
||||||
c.UpdateTimestamp(msg.Time())
|
c.authorID = msg.Author().ID()
|
||||||
c.UpdateAuthor(msg.Author())
|
|
||||||
c.UpdateContent(msg.Content())
|
|
||||||
|
|
||||||
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
||||||
c.nonce = noncer.Nonce()
|
c.nonce = noncer.Nonce()
|
||||||
|
@ -102,7 +108,6 @@ func (m *GenericContainer) UpdateTimestamp(t time.Time) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) UpdateAuthor(author cchat.MessageAuthor) {
|
func (m *GenericContainer) UpdateAuthor(author cchat.MessageAuthor) {
|
||||||
m.authorID = author.ID()
|
|
||||||
m.UpdateAuthorName(author.Name())
|
m.UpdateAuthorName(author.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type PresendContainer interface {
|
type PresendContainer interface {
|
||||||
Container
|
|
||||||
SetID(id string)
|
SetID(id string)
|
||||||
SetDone()
|
SetDone()
|
||||||
SetSentError(err error)
|
SetSentError(err error)
|
||||||
|
@ -24,7 +23,10 @@ type GenericPresendContainer struct {
|
||||||
var _ PresendContainer = (*GenericPresendContainer)(nil)
|
var _ PresendContainer = (*GenericPresendContainer)(nil)
|
||||||
|
|
||||||
func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
|
func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
|
||||||
c := NewEmptyContainer()
|
return WrapPresendContainer(NewEmptyContainer(), msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapPresendContainer(c *GenericContainer, msg input.PresendMessage) *GenericPresendContainer {
|
||||||
c.nonce = msg.Nonce()
|
c.nonce = msg.Nonce()
|
||||||
c.authorID = msg.AuthorID()
|
c.authorID = msg.AuthorID()
|
||||||
c.UpdateContent(text.Rich{Content: msg.Content()})
|
c.UpdateContent(text.Rich{Content: msg.Content()})
|
||||||
|
@ -52,6 +54,8 @@ func (m *GenericPresendContainer) SetDone() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericPresendContainer) SetSentError(err error) {
|
func (m *GenericPresendContainer) SetSentError(err error) {
|
||||||
m.Content.SetMarkup(`<span color="red">` + html.EscapeString(m.Content.GetLabel()) + `</span>`)
|
var content = html.EscapeString(m.Content.GetLabel())
|
||||||
|
|
||||||
|
m.Content.SetMarkup(`<span color="red">` + content + `</span>`)
|
||||||
m.Content.SetTooltipText(err.Error())
|
m.Content.SetTooltipText(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
|
"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/container/cozy"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -34,7 +34,8 @@ func NewView() *View {
|
||||||
view := &View{}
|
view := &View{}
|
||||||
|
|
||||||
// TODO: change
|
// TODO: change
|
||||||
view.Container = compact.NewContainer()
|
// view.Container = compact.NewContainer()
|
||||||
|
view.Container = cozy.NewContainer()
|
||||||
view.SendInput = input.NewField(view)
|
view.SendInput = input.NewField(view)
|
||||||
|
|
||||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
|
|
@ -40,3 +40,21 @@ func SetImageIcon(img *gtk.Image, icon string, sizepx int) {
|
||||||
img.SetProperty("pixel-size", sizepx)
|
img.SetProperty("pixel-size", sizepx)
|
||||||
img.SetSizeRequest(sizepx, sizepx)
|
img.SetSizeRequest(sizepx, sizepx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MenuItem struct {
|
||||||
|
Name string
|
||||||
|
Fn func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendMenuItems(menu interface{ Append(gtk.IMenuItem) }, items []MenuItem) {
|
||||||
|
for _, item := range items {
|
||||||
|
menu.Append(NewMenuItem(item.Name, item.Fn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMenuItem(label string, fn func()) *gtk.MenuItem {
|
||||||
|
mb, _ := gtk.MenuItemNewWithLabel(label)
|
||||||
|
mb.Show()
|
||||||
|
mb.Connect("activate", fn)
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
|
@ -48,7 +48,21 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread-unsafe methods should only be called right after construction.
|
// URL is not thread-safe.
|
||||||
|
func (i *Icon) URL() string {
|
||||||
|
return i.url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Icon) CopyPixbuf(dst httputil.ImageContainer) {
|
||||||
|
switch i.Image.GetStorageType() {
|
||||||
|
case gtk.IMAGE_PIXBUF:
|
||||||
|
dst.SetFromPixbuf(i.Image.GetPixbuf())
|
||||||
|
case gtk.IMAGE_ANIMATION:
|
||||||
|
dst.SetFromAnimation(i.Image.GetAnimation())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread-unsafe setter methods should only be called right after construction.
|
||||||
|
|
||||||
// SetPlaceholderIcon is not thread-safe.
|
// SetPlaceholderIcon is not thread-safe.
|
||||||
func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
|
func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
|
||||||
|
@ -73,7 +87,14 @@ func (i *Icon) AddProcessors(procs ...imgutil.Processor) {
|
||||||
|
|
||||||
// SetIcon is thread-safe.
|
// SetIcon is thread-safe.
|
||||||
func (i *Icon) SetIcon(url string) {
|
func (i *Icon) SetIcon(url string) {
|
||||||
gts.ExecAsync(func() { i.SetRevealChild(true) })
|
gts.ExecAsync(func() {
|
||||||
|
i.SetIconUnsafe(url)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIconUnsafe is not thread-safe.
|
||||||
|
func (i *Icon) SetIconUnsafe(url string) {
|
||||||
|
i.SetRevealChild(true)
|
||||||
i.url = url
|
i.url = url
|
||||||
i.updateAsync()
|
i.updateAsync()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package rich
|
package rich
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
@ -11,9 +13,17 @@ import (
|
||||||
|
|
||||||
// TODO: parser
|
// TODO: parser
|
||||||
|
|
||||||
|
func MakeRed(content text.Rich) string {
|
||||||
|
return `<span color="red">` + html.EscapeString(content.Content) + `</span>`
|
||||||
|
}
|
||||||
|
|
||||||
type Labeler interface {
|
type Labeler interface {
|
||||||
|
// thread-safe
|
||||||
cchat.LabelContainer // thread-safe
|
cchat.LabelContainer // thread-safe
|
||||||
GetLabel() text.Rich // not thread-safe
|
|
||||||
|
// not thread-safe
|
||||||
|
SetLabelUnsafe(text.Rich)
|
||||||
|
GetLabel() text.Rich
|
||||||
GetText() string
|
GetText() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
internal/ui/service/children.go
Normal file
30
internal/ui/service/children.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type children struct {
|
||||||
|
*gtk.Box
|
||||||
|
Sessions map[string]*session.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChildren() *children {
|
||||||
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
box.Show()
|
||||||
|
|
||||||
|
return &children{box, map[string]*session.Row{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *children) addSessionRow(id string, row *session.Row) {
|
||||||
|
c.Sessions[id] = row
|
||||||
|
c.Box.Add(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *children) removeSessionRow(id string) {
|
||||||
|
if row, ok := c.Sessions[id]; ok {
|
||||||
|
delete(c.Sessions, id)
|
||||||
|
c.Box.Remove(row)
|
||||||
|
}
|
||||||
|
}
|
60
internal/ui/service/header.go
Normal file
60
internal/ui/service/header.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
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/rich"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/imgutil"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const IconSize = 32
|
||||||
|
|
||||||
|
type header struct {
|
||||||
|
*gtk.Box
|
||||||
|
reveal *rich.ToggleButtonImage // no rich text here but it's left aligned
|
||||||
|
add *gtk.Button
|
||||||
|
|
||||||
|
Menu *gtk.Menu
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeader(svc cchat.Service) *header {
|
||||||
|
reveal := rich.NewToggleButtonImage(text.Rich{Content: svc.Name()})
|
||||||
|
reveal.Box.SetHAlign(gtk.ALIGN_START)
|
||||||
|
reveal.Image.AddProcessors(imgutil.Round(true))
|
||||||
|
reveal.Image.SetPlaceholderIcon("folder-remote-symbolic", IconSize)
|
||||||
|
reveal.SetRelief(gtk.RELIEF_NONE)
|
||||||
|
reveal.SetMode(true)
|
||||||
|
reveal.Show()
|
||||||
|
|
||||||
|
add, _ := gtk.ButtonNewFromIconName("list-add-symbolic", gtk.ICON_SIZE_BUTTON)
|
||||||
|
add.SetRelief(gtk.RELIEF_NONE)
|
||||||
|
add.SetSizeRequest(IconSize, IconSize)
|
||||||
|
add.Show()
|
||||||
|
|
||||||
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
box.PackStart(reveal, true, true, 0)
|
||||||
|
box.PackStart(add, false, false, 0)
|
||||||
|
box.Show()
|
||||||
|
|
||||||
|
if iconer, ok := svc.(cchat.Icon); ok {
|
||||||
|
if err := iconer.Icon(reveal); err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "Error getting session logo"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menu, _ := gtk.MenuNew()
|
||||||
|
|
||||||
|
// Spawn the menu on right click.
|
||||||
|
reveal.Connect("event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
||||||
|
if gts.EventIsRightClick(ev) {
|
||||||
|
menu.PopupAtPointer(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return &header{box, reveal, add, menu}
|
||||||
|
}
|
|
@ -2,21 +2,23 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/keyring"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"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/breadcrumb"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const IconSize = 32
|
|
||||||
|
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
session.Controller
|
session.Controller
|
||||||
|
|
||||||
|
// MessageRowSelected is wrapped around session's MessageRowSelected.
|
||||||
|
MessageRowSelected(*session.Row, *server.Row, cchat.ServerMessage)
|
||||||
|
// AuthenticateSession is called to spawn the authentication dialog.
|
||||||
AuthenticateSession(*Container, cchat.Service)
|
AuthenticateSession(*Container, cchat.Service)
|
||||||
|
// SaveAllSessions is called to save all available sessions from the menu.
|
||||||
|
SaveAllSessions(*Container)
|
||||||
}
|
}
|
||||||
|
|
||||||
type View struct {
|
type View struct {
|
||||||
|
@ -52,10 +54,14 @@ func (v *View) AddService(svc cchat.Service, ctrl Controller) *Container {
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
|
Service cchat.Service
|
||||||
|
|
||||||
header *header
|
header *header
|
||||||
revealer *gtk.Revealer
|
revealer *gtk.Revealer
|
||||||
children *children
|
children *children
|
||||||
rowctrl Controller
|
|
||||||
|
// Embed controller and extend it to override RestoreSession.
|
||||||
|
Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer(svc cchat.Service, ctrl Controller) *Container {
|
func NewContainer(svc cchat.Service, ctrl Controller) *Container {
|
||||||
|
@ -76,7 +82,14 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
|
||||||
|
|
||||||
primitives.AddClass(box, "service")
|
primitives.AddClass(box, "service")
|
||||||
|
|
||||||
var container = &Container{box, header, chrev, children, ctrl}
|
container := &Container{
|
||||||
|
Box: box,
|
||||||
|
Service: svc,
|
||||||
|
header: header,
|
||||||
|
revealer: chrev,
|
||||||
|
children: children,
|
||||||
|
Controller: ctrl,
|
||||||
|
}
|
||||||
|
|
||||||
// On click, toggle reveal.
|
// On click, toggle reveal.
|
||||||
header.reveal.Connect("clicked", func() {
|
header.reveal.Connect("clicked", func() {
|
||||||
|
@ -90,71 +103,42 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
|
||||||
ctrl.AuthenticateSession(container, svc)
|
ctrl.AuthenticateSession(container, svc)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Make menu items.
|
||||||
|
primitives.AppendMenuItems(header.Menu, []primitives.MenuItem{
|
||||||
|
{Name: "Save Sessions", Fn: func() {
|
||||||
|
ctrl.SaveAllSessions(container)
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
return container
|
return container
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) AddSession(ses cchat.Session) {
|
func (c *Container) AddSession(ses cchat.Session) *session.Row {
|
||||||
srow := session.New(c, ses, c.rowctrl)
|
srow := session.New(c, ses, c)
|
||||||
c.children.addSessionRow(srow)
|
c.children.addSessionRow(ses.ID(), srow)
|
||||||
|
|
||||||
|
return srow
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) Sessions() []cchat.Session {
|
func (c *Container) AddLoadingSession(id, name string) *session.Row {
|
||||||
var sessions = make([]cchat.Session, len(c.children.Sessions))
|
srow := session.NewLoading(c, name, c)
|
||||||
for i, s := range c.children.Sessions {
|
c.children.addSessionRow(id, srow)
|
||||||
sessions[i] = s.Session
|
|
||||||
|
return srow
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyringSessions returns all known keyring sessions. Sessions that can't be
|
||||||
|
// saved will not be in the slice.
|
||||||
|
func (c *Container) KeyringSessions() []keyring.Session {
|
||||||
|
var ksessions = make([]keyring.Session, 0, len(c.children.Sessions))
|
||||||
|
for _, s := range c.children.Sessions {
|
||||||
|
if k := s.KeyringSession(); k != nil {
|
||||||
|
ksessions = append(ksessions, *k)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return sessions
|
return ksessions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) Breadcrumb() breadcrumb.Breadcrumb {
|
func (c *Container) Breadcrumb() breadcrumb.Breadcrumb {
|
||||||
return breadcrumb.Try(nil, c.header.reveal.GetText())
|
return breadcrumb.Try(nil, c.header.reveal.GetText())
|
||||||
}
|
}
|
||||||
|
|
||||||
type header struct {
|
|
||||||
*gtk.Box
|
|
||||||
reveal *rich.ToggleButtonImage // no rich text here but it's left aligned
|
|
||||||
add *gtk.Button
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHeader(svc cchat.Service) *header {
|
|
||||||
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()
|
|
||||||
|
|
||||||
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)
|
|
||||||
add.Show()
|
|
||||||
|
|
||||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
|
||||||
box.PackStart(reveal, true, true, 0)
|
|
||||||
box.PackStart(add, false, false, 0)
|
|
||||||
box.Show()
|
|
||||||
|
|
||||||
return &header{box, reveal, add}
|
|
||||||
}
|
|
||||||
|
|
||||||
type children struct {
|
|
||||||
*gtk.Box
|
|
||||||
Sessions []*session.Row
|
|
||||||
}
|
|
||||||
|
|
||||||
func newChildren() *children {
|
|
||||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
|
||||||
box.Show()
|
|
||||||
|
|
||||||
return &children{box, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *children) addSessionRow(row *session.Row) {
|
|
||||||
c.Sessions = append(c.Sessions, row)
|
|
||||||
c.Box.Add(row)
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
"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/breadcrumb"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -37,6 +38,7 @@ type Row struct {
|
||||||
func NewRow(parent breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller) *Row {
|
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.Box.SetHAlign(gtk.ALIGN_START)
|
||||||
|
button.Image.AddProcessors(imgutil.Round(true))
|
||||||
button.Image.SetSize(IconSize)
|
button.Image.SetSize(IconSize)
|
||||||
button.SetRelief(gtk.RELIEF_NONE)
|
button.SetRelief(gtk.RELIEF_NONE)
|
||||||
button.Show()
|
button.Show()
|
||||||
|
|
|
@ -2,12 +2,16 @@ package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/keyring"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
"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/breadcrumb"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const IconSize = 32
|
const IconSize = 32
|
||||||
|
@ -15,6 +19,7 @@ const IconSize = 32
|
||||||
// Controller extends server.RowController to add session.
|
// Controller extends server.RowController to add session.
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
MessageRowSelected(*Row, *server.Row, cchat.ServerMessage)
|
MessageRowSelected(*Row, *server.Row, cchat.ServerMessage)
|
||||||
|
RestoreSession(*Row, cchat.SessionRestorer) // async
|
||||||
}
|
}
|
||||||
|
|
||||||
type Row struct {
|
type Row struct {
|
||||||
|
@ -29,16 +34,30 @@ type Row struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Row {
|
func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Row {
|
||||||
|
row := new(parent, ctrl)
|
||||||
|
row.SetSession(ses)
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLoading(parent breadcrumb.Breadcrumber, name string, ctrl Controller) *Row {
|
||||||
|
row := new(parent, ctrl)
|
||||||
|
row.Button.SetLabelUnsafe(text.Rich{Content: name})
|
||||||
|
row.Button.Image.SetPlaceholderIcon("content-loading-symbolic", IconSize)
|
||||||
|
row.SetSensitive(false)
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func new(parent breadcrumb.Breadcrumber, ctrl Controller) *Row {
|
||||||
row := &Row{
|
row := &Row{
|
||||||
Session: ses,
|
ctrl: ctrl,
|
||||||
ctrl: ctrl,
|
parent: parent,
|
||||||
parent: parent,
|
|
||||||
}
|
}
|
||||||
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.Box.SetHAlign(gtk.ALIGN_START)
|
||||||
row.Button.Image.SetPlaceholderIcon("user-available-symbolic", IconSize)
|
row.Button.Image.AddProcessors(imgutil.Round(true))
|
||||||
|
// Set the loading icon.
|
||||||
row.Button.SetRelief(gtk.RELIEF_NONE)
|
row.Button.SetRelief(gtk.RELIEF_NONE)
|
||||||
// On click, toggle reveal.
|
// On click, toggle reveal.
|
||||||
row.Button.Connect("clicked", func() {
|
row.Button.Connect("clicked", func() {
|
||||||
|
@ -47,12 +66,10 @@ func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Ro
|
||||||
row.Button.SetActive(revealed)
|
row.Button.SetActive(revealed)
|
||||||
})
|
})
|
||||||
row.Button.Show()
|
row.Button.Show()
|
||||||
row.Button.Try(ses, "session")
|
|
||||||
|
|
||||||
row.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
row.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
row.Box.SetMarginStart(server.ChildrenMargin)
|
row.Box.SetMarginStart(server.ChildrenMargin)
|
||||||
row.Box.PackStart(row.Button, false, false, 0)
|
row.Box.PackStart(row.Button, false, false, 0)
|
||||||
row.Box.PackStart(row.Servers, false, false, 0)
|
|
||||||
row.Box.Show()
|
row.Box.Show()
|
||||||
|
|
||||||
primitives.AddClass(row.Box, "session")
|
primitives.AddClass(row.Box, "session")
|
||||||
|
@ -60,6 +77,48 @@ func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Ro
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyringSession returns a keyring session, or nil if the session cannot be
|
||||||
|
// saved. This function is not cached, as I'd rather not keep the map in memory.
|
||||||
|
func (r *Row) KeyringSession() *keyring.Session {
|
||||||
|
// Is the session saveable?
|
||||||
|
saver, ok := r.Session.(cchat.SessionSaver)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ks := keyring.Session{
|
||||||
|
ID: r.Session.ID(),
|
||||||
|
Name: r.Button.GetText(),
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := saver.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(errors.Wrapf(err, "Failed to save session ID %s (%s)", ks.ID, ks.Name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ks.Data = s
|
||||||
|
|
||||||
|
return &ks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Row) SetSession(ses cchat.Session) {
|
||||||
|
r.Session = ses
|
||||||
|
r.Servers = server.NewChildren(r, ses, r)
|
||||||
|
r.Button.Image.SetPlaceholderIcon("user-available-symbolic", IconSize)
|
||||||
|
r.Box.PackStart(r.Servers, false, false, 0)
|
||||||
|
r.SetSensitive(true)
|
||||||
|
|
||||||
|
// Set the session's name to the button.
|
||||||
|
r.Button.Try(ses, "session")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Row) SetFailed(err error) {
|
||||||
|
r.SetTooltipText(err.Error())
|
||||||
|
// TODO: setting the label directly here is kind of shitty, as it screws up
|
||||||
|
// the getter. Fix?
|
||||||
|
r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel()))
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Row) MessageRowSelected(server *server.Row, smsg cchat.ServerMessage) {
|
func (r *Row) MessageRowSelected(server *server.Row, smsg cchat.ServerMessage) {
|
||||||
r.ctrl.MessageRowSelected(r, server, smsg)
|
r.ctrl.MessageRowSelected(r, server, smsg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/markbates/pkger"
|
"github.com/markbates/pkger"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -46,8 +47,45 @@ func NewApplication() *App {
|
||||||
func (app *App) AddService(svc cchat.Service) {
|
func (app *App) AddService(svc cchat.Service) {
|
||||||
var container = app.window.Services.AddService(svc, app)
|
var container = app.window.Services.AddService(svc, app)
|
||||||
|
|
||||||
// Attempt to restore sessions asynchronously.
|
// Can this session be restored? If not, exit.
|
||||||
keyring.RestoreSessions(svc, container.AddSession)
|
restorer, ok := container.Service.(cchat.SessionRestorer)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessions = keyring.RestoreSessions(container.Service.Name())
|
||||||
|
|
||||||
|
for _, krs := range sessions {
|
||||||
|
// Copy the session to avoid race conditions.
|
||||||
|
krs := krs
|
||||||
|
row := container.AddLoadingSession(krs.ID, krs.Name)
|
||||||
|
|
||||||
|
go app.restoreSession(row, restorer, krs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreSession attempts to restore the session asynchronously.
|
||||||
|
func (app *App) RestoreSession(row *session.Row, r cchat.SessionRestorer) {
|
||||||
|
// Get the restore data.
|
||||||
|
ks := row.KeyringSession()
|
||||||
|
if ks == nil {
|
||||||
|
log.Warn(errors.New("Attempted restore in ui.go"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go app.restoreSession(row, r, *ks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// synchronous op
|
||||||
|
func (app *App) restoreSession(row *session.Row, r cchat.SessionRestorer, k keyring.Session) {
|
||||||
|
s, err := r.RestoreSession(k.Data)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrapf(err, "Failed to restore session %s (%s)", k.ID, k.Name)
|
||||||
|
log.Error(err)
|
||||||
|
|
||||||
|
gts.ExecAsync(func() { row.SetFailed(err) })
|
||||||
|
} else {
|
||||||
|
gts.ExecAsync(func() { row.SetSession(s) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) MessageRowSelected(ses *session.Row, srv *server.Row, smsg cchat.ServerMessage) {
|
func (app *App) MessageRowSelected(ses *session.Row, srv *server.Row, smsg cchat.ServerMessage) {
|
||||||
|
@ -70,13 +108,15 @@ func (app *App) AuthenticateSession(container *service.Container, svc cchat.Serv
|
||||||
auth.NewDialog(svc.Name(), svc.Authenticate(), func(ses cchat.Session) {
|
auth.NewDialog(svc.Name(), svc.Authenticate(), func(ses cchat.Session) {
|
||||||
container.AddSession(ses)
|
container.AddSession(ses)
|
||||||
|
|
||||||
// Save all sessions.
|
// Try and save all keyring sessions.
|
||||||
for _, err := range keyring.SaveSessions(svc, container.Sessions()) {
|
app.SaveAllSessions(container)
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) SaveAllSessions(container *service.Container) {
|
||||||
|
keyring.SaveSessions(container.Service.Name(), container.KeyringSessions())
|
||||||
|
}
|
||||||
|
|
||||||
func (app *App) Header() gtk.IWidget {
|
func (app *App) Header() gtk.IWidget {
|
||||||
return app.header
|
return app.header
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue