Session restore and icons added

This commit is contained in:
diamondburned (Forefront) 2020-06-06 21:27:28 -07:00
parent 2d628fbbb3
commit 43aa4dcef3
26 changed files with 800 additions and 322 deletions

2
go.mod
View File

@ -4,6 +4,8 @@ go 1.14
replace github.com/diamondburned/cchat-mock => ../cchat-mock/
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200606223630-b0c33ec7b10a
require (
github.com/Xuanwo/go-locale v0.2.0
github.com/diamondburned/cchat v0.0.15

2
go.sum
View File

@ -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/diamondburned/cchat v0.0.15 h1:1o4OX8zw/CdSv3Idaylz7vjHVOZKEi/xkg8BpEvtsHY=
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/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
github.com/die-net/lrucache v0.0.0-20190707192454-883874fe3947 h1:U/5Sq2nJQ0XDyks+8ATghtHSuquIGq7JYrqSrvtR2dg=

View File

@ -2,9 +2,9 @@ package gts
import (
"os"
"sync"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
)
@ -12,7 +12,6 @@ import (
const AppID = "com.github.diamondburned.cchat-gtk"
var Args = append([]string{}, os.Args...)
var recvPool *sync.Pool
var App struct {
*gtk.Application
@ -22,13 +21,6 @@ var App struct {
func init() {
gtk.Init(&Args)
recvPool = &sync.Pool{
New: func() interface{} {
return make(chan struct{})
},
}
App.Application, _ = gtk.ApplicationNew(AppID, 0)
}
@ -98,8 +90,7 @@ func ExecAsync(fn func()) {
// ExecSync executes the function asynchronously, but returns a channel that
// indicates when the job is done.
func ExecSync(fn func()) <-chan struct{} {
ch := recvPool.Get().(chan struct{})
defer recvPool.Put(ch)
var ch = make(chan struct{})
glib.IdleAdd(func() {
fn()
@ -108,3 +99,8 @@ func ExecSync(fn func()) <-chan struct{} {
return ch
}
func EventIsRightClick(ev *gdk.Event) bool {
keyev := gdk.EventButtonNewFromEvent(ev)
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
}

View File

@ -8,16 +8,68 @@ import (
"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...)
type ImageContainer interface {
SetFromPixbuf(*gdk.Pixbuf)
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)
if err != nil {
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
// 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)
}
})
<-gts.ExecSync(func() {
middle(l, gif)
})
// 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"))
}
}
// AsyncImageSized resizes using GdkPixbuf. This method does not use the cache.
func AsyncImageSized(img *gtk.Image, url string, w, h int, procs ...imgutil.Processor) {
// TODO
panic("TODO")
}

View File

@ -5,14 +5,12 @@ import (
"encoding/gob"
"strings"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/pkg/errors"
"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)
if err != nil {
return err
@ -33,57 +31,24 @@ func set(service string, v interface{}) error {
return keyring.Set("cchat-gtk", service, b.String())
}
func SaveSessions(service cchat.Service, sessions []cchat.Session) (saveErrs []error) {
var sessionData = make([]map[string]string, 0, len(sessions))
type Session struct {
ID string
Name string
Data map[string]string
}
for _, session := range sessions {
sv, ok := session.(cchat.SessionSaver)
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 {
func SaveSessions(serviceName string, sessions []Session) {
if err := set(serviceName, sessions); err != nil {
log.Warn(errors.Wrap(err, "Error saving session"))
saveErrs = append(saveErrs, err)
}
return
}
// RestoreSessions restores all sessions of the service asynchronously, then
// calls the auth callback inside the Gtk main thread.
func RestoreSessions(service cchat.Service, auth func(cchat.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
func RestoreSessions(serviceName string) (sessions []Session) {
// 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)
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
}

View File

@ -11,15 +11,17 @@ type Container struct {
}
func NewContainer() *Container {
c := &Container{}
c.GridContainer = container.NewGridContainer(c)
return c
return &Container{
GridContainer: container.NewGridContainer(constructor{}),
}
}
func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
type constructor struct{}
func (constructor) NewMessage(msg cchat.MessageCreate) container.GridMessage {
return NewMessage(msg)
}
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
func (constructor) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
return NewPresendMessage(msg)
}

View File

@ -9,17 +9,17 @@ import (
)
type PresendMessage struct {
*message.GenericPresendContainer
message.PresendContainer
Message
}
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
return PresendMessage{
GenericPresendContainer: message.NewPresendContainer(msg),
}
}
var msgc = message.NewPresendContainer(msg)
func (p PresendMessage) Attach(grid *gtk.Grid, row int) {
attachGenericContainer(p.GenericContainer, grid, row)
return PresendMessage{
PresendContainer: msgc,
Message: Message{msgc.GenericContainer},
}
}
type Message struct {
@ -29,15 +29,13 @@ type Message struct {
var _ container.GridMessage = (*Message)(nil)
func NewMessage(msg cchat.MessageCreate) Message {
return Message{
GenericContainer: message.NewContainer(msg),
}
msgc := message.NewContainer(msg)
message.FillContainer(msgc, msg)
return Message{msgc}
}
func NewEmptyMessage() Message {
return Message{
GenericContainer: message.NewEmptyContainer(),
}
return Message{message.NewEmptyContainer()}
}
// 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) {
grid.Attach(m.Timestamp, 0, row, 1, 1)
grid.Attach(m.Username, 1, row, 1, 1)
grid.Attach(m.Content, 2, row, 1, 1)
container.AttachRow(grid, row, m.Timestamp, m.Username, m.Content)
}

View File

@ -21,13 +21,6 @@ type PresendGridMessage interface {
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 {
@ -47,6 +40,14 @@ type Container interface {
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
// message grids.
type GridContainer struct {
@ -55,8 +56,15 @@ type GridContainer struct {
construct Constructor
messages map[string]*gridMessage
nonceMsgs map[string]*gridMessage
messages []*gridMessage // sync w/ grid rows
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 (
@ -66,7 +74,7 @@ var (
func NewGridContainer(constr Constructor) *GridContainer {
grid, _ := gtk.GridNew()
grid.SetColumnSpacing(10)
grid.SetColumnSpacing(ColumnSpacing)
grid.SetRowSpacing(5)
grid.SetMarginStart(5)
grid.SetMarginEnd(5)
@ -82,42 +90,42 @@ func NewGridContainer(constr Constructor) *GridContainer {
ScrolledWindow: sw,
Main: grid,
construct: constr,
messages: map[string]*gridMessage{},
nonceMsgs: map[string]*gridMessage{},
messageIDs: map[string]int{},
nonceMsgs: map[string]int{},
}
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.Main.GetChildren().Foreach(func(v interface{}) {
// Unsafe assertion ftw.
c.Main.Remove(v.(gtk.IWidget))
})
c.messages = map[string]*gridMessage{}
c.nonceMsgs = map[string]*gridMessage{}
c.messages = nil
c.messageIDs = map[string]int{}
c.nonceMsgs = map[string]int{}
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{
msgc := &gridMessage{
GridMessage: presend,
presend: presend,
index: c.len(),
}
c.nonceMsgs[presend.Nonce()] = &msgc
msgc.Attach(c.Main, msgc.index)
// Grab index before appending, as that'll be where the added message is.
index := len(c.messages)
c.messages = append(c.messages, msgc)
c.nonceMsgs[presend.Nonce()] = index
msgc.Attach(c.Main, index)
return func(err error) {
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.
func (c *GridContainer) FindMessage(msg cchat.MessageHeader) GridMessage {
if m := c.findMessage(msg); m != nil {
// FindMessage iterates backwards and returns the message if isMessage() returns
// true on that message.
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 nil
}
func (c *GridContainer) findMessage(msg cchat.MessageHeader) *gridMessage {
func (c *GridContainer) message(msg cchat.MessageHeader) *gridMessage {
// Search using the ID first.
m, ok := c.messages[msg.ID()]
i, ok := c.messageIDs[msg.ID()]
if ok {
return m
return c.messages[i]
}
// Is this an existing message?
@ -147,11 +167,14 @@ func (c *GridContainer) findMessage(msg cchat.MessageHeader) *gridMessage {
var nonce = noncer.Nonce()
// Things in this map are guaranteed to have presend != nil.
m, ok := c.nonceMsgs[nonce]
i, ok := c.nonceMsgs[nonce]
if ok {
// Move the message outside nonceMsgs.
// Move the message outside nonceMsgs and into messageIDs.
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.
m.presend.SetID(msg.ID())
@ -168,27 +191,31 @@ func (c *GridContainer) findMessage(msg cchat.MessageHeader) *gridMessage {
func (c *GridContainer) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
// Attempt update before insert (aka upsert).
if msgc := c.FindMessage(msg); msgc != nil {
// Attempt to update before insertion (aka upsert).
if msgc := c.Message(msg); msgc != nil {
msgc.UpdateAuthor(msg.Author())
msgc.UpdateContent(msg.Content())
msgc.UpdateTimestamp(msg.Time())
return
}
msgc := gridMessage{
msgc := &gridMessage{
GridMessage: c.construct.NewMessage(msg),
index: c.len(),
}
c.messages[msgc.ID()] = &msgc
msgc.Attach(c.Main, msgc.index)
// Grab index before appending, as that'll be where the added message is.
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) {
gts.ExecAsync(func() {
if msgc := c.FindMessage(msg); msgc != nil {
if msgc := c.Message(msg); msgc != nil {
if author := msg.Author(); author != nil {
msgc.UpdateAuthor(author)
}
@ -202,9 +229,13 @@ func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
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)
if i, ok := c.messageIDs[msg.ID()]; ok {
// Remove off the slice.
c.messages = append(c.messages[:i], c.messages[i+1:]...)
// Remove off the map.
delete(c.messageIDs, msg.ID())
c.Main.RemoveRow(i)
}
})
}

View File

@ -1,13 +1,62 @@
package cozy
import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/autoscroll"
"github.com/gotk3/gotk3/gtk"
"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"
)
type AvatarPixbufCopier interface {
CopyAvatarPixbuf(httputil.ImageContainer)
}
const (
AvatarSize = 40
AvatarMargin = 10
)
type Container struct {
*autoscroll.ScrolledWindow
main *gtk.Grid
messages map[string]Message
nonceMsgs map[string]Message
*container.GridContainer
}
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
}

View File

@ -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()

View 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
}

View 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),
}
}

View File

@ -4,51 +4,10 @@ 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/gotk3/gotk3/gtk"
"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 {
*gtk.Box
username *usernameContainer
@ -117,18 +76,8 @@ func NewField(ctrl Controller) *Field {
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
f.UserID = session.ID()
// Does sender (aka Server) implement ServerNickname?
var err error
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)
}
// Update the left username container in the input.
f.username.Update(session, sender)
// Set the sender.
f.sender = sender

View 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)
}
})
}

View File

@ -22,6 +22,12 @@ type Container interface {
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
// to use.
type GenericContainer struct {
@ -36,12 +42,12 @@ type GenericContainer struct {
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 {
c := NewEmptyContainer()
c.id = msg.ID()
c.UpdateTimestamp(msg.Time())
c.UpdateAuthor(msg.Author())
c.UpdateContent(msg.Content())
c.authorID = msg.Author().ID()
if noncer, ok := msg.(cchat.MessageNonce); ok {
c.nonce = noncer.Nonce()
@ -102,7 +108,6 @@ func (m *GenericContainer) UpdateTimestamp(t time.Time) {
}
func (m *GenericContainer) UpdateAuthor(author cchat.MessageAuthor) {
m.authorID = author.ID()
m.UpdateAuthorName(author.Name())
}

View File

@ -9,7 +9,6 @@ import (
)
type PresendContainer interface {
Container
SetID(id string)
SetDone()
SetSentError(err error)
@ -24,7 +23,10 @@ type GenericPresendContainer struct {
var _ PresendContainer = (*GenericPresendContainer)(nil)
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.authorID = msg.AuthorID()
c.UpdateContent(text.Rich{Content: msg.Content()})
@ -52,6 +54,8 @@ func (m *GenericPresendContainer) SetDone() {
}
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())
}

View File

@ -4,7 +4,7 @@ import (
"github.com/diamondburned/cchat"
"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/compact"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/cozy"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
@ -34,7 +34,8 @@ func NewView() *View {
view := &View{}
// TODO: change
view.Container = compact.NewContainer()
// view.Container = compact.NewContainer()
view.Container = cozy.NewContainer()
view.SendInput = input.NewField(view)
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)

View File

@ -40,3 +40,21 @@ func SetImageIcon(img *gtk.Image, icon string, sizepx int) {
img.SetProperty("pixel-size", 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
}

View File

@ -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.
func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
@ -73,7 +87,14 @@ func (i *Icon) AddProcessors(procs ...imgutil.Processor) {
// SetIcon is thread-safe.
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.updateAsync()
}

View File

@ -1,6 +1,8 @@
package rich
import (
"html"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
@ -11,9 +13,17 @@ import (
// TODO: parser
func MakeRed(content text.Rich) string {
return `<span color="red">` + html.EscapeString(content.Content) + `</span>`
}
type Labeler interface {
// thread-safe
cchat.LabelContainer // thread-safe
GetLabel() text.Rich // not thread-safe
// not thread-safe
SetLabelUnsafe(text.Rich)
GetLabel() text.Rich
GetText() string
}

View 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)
}
}

View 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}
}

View File

@ -2,21 +2,23 @@ package service
import (
"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/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/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const IconSize = 32
type Controller interface {
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)
// SaveAllSessions is called to save all available sessions from the menu.
SaveAllSessions(*Container)
}
type View struct {
@ -52,10 +54,14 @@ func (v *View) AddService(svc cchat.Service, ctrl Controller) *Container {
type Container struct {
*gtk.Box
Service cchat.Service
header *header
revealer *gtk.Revealer
children *children
rowctrl Controller
// Embed controller and extend it to override RestoreSession.
Controller
}
func NewContainer(svc cchat.Service, ctrl Controller) *Container {
@ -76,7 +82,14 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
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.
header.reveal.Connect("clicked", func() {
@ -90,71 +103,42 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
ctrl.AuthenticateSession(container, svc)
})
// Make menu items.
primitives.AppendMenuItems(header.Menu, []primitives.MenuItem{
{Name: "Save Sessions", Fn: func() {
ctrl.SaveAllSessions(container)
}},
})
return container
}
func (c *Container) AddSession(ses cchat.Session) {
srow := session.New(c, ses, c.rowctrl)
c.children.addSessionRow(srow)
func (c *Container) AddSession(ses cchat.Session) *session.Row {
srow := session.New(c, ses, c)
c.children.addSessionRow(ses.ID(), srow)
return srow
}
func (c *Container) Sessions() []cchat.Session {
var sessions = make([]cchat.Session, len(c.children.Sessions))
for i, s := range c.children.Sessions {
sessions[i] = s.Session
func (c *Container) AddLoadingSession(id, name string) *session.Row {
srow := session.NewLoading(c, name, c)
c.children.addSessionRow(id, srow)
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 {
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)
}

View File

@ -8,6 +8,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
@ -37,6 +38,7 @@ type Row struct {
func NewRow(parent breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller) *Row {
button := rich.NewToggleButtonImage(text.Rich{})
button.Box.SetHAlign(gtk.ALIGN_START)
button.Image.AddProcessors(imgutil.Round(true))
button.Image.SetSize(IconSize)
button.SetRelief(gtk.RELIEF_NONE)
button.Show()

View File

@ -2,12 +2,16 @@ package session
import (
"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/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/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const IconSize = 32
@ -15,6 +19,7 @@ const IconSize = 32
// Controller extends server.RowController to add session.
type Controller interface {
MessageRowSelected(*Row, *server.Row, cchat.ServerMessage)
RestoreSession(*Row, cchat.SessionRestorer) // async
}
type Row struct {
@ -29,16 +34,30 @@ type Row struct {
}
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{
Session: ses,
ctrl: ctrl,
parent: parent,
ctrl: ctrl,
parent: parent,
}
row.Servers = server.NewChildren(row, ses, row)
row.Button = rich.NewToggleButtonImage(text.Rich{})
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)
// On click, toggle reveal.
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.Show()
row.Button.Try(ses, "session")
row.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
row.Box.SetMarginStart(server.ChildrenMargin)
row.Box.PackStart(row.Button, false, false, 0)
row.Box.PackStart(row.Servers, false, false, 0)
row.Box.Show()
primitives.AddClass(row.Box, "session")
@ -60,6 +77,48 @@ func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Ro
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) {
r.ctrl.MessageRowSelected(r, server, smsg)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/gotk3/gotk3/gtk"
"github.com/markbates/pkger"
"github.com/pkg/errors"
)
func init() {
@ -46,8 +47,45 @@ func NewApplication() *App {
func (app *App) AddService(svc cchat.Service) {
var container = app.window.Services.AddService(svc, app)
// Attempt to restore sessions asynchronously.
keyring.RestoreSessions(svc, container.AddSession)
// Can this session be restored? If not, exit.
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) {
@ -70,13 +108,15 @@ func (app *App) AuthenticateSession(container *service.Container, svc cchat.Serv
auth.NewDialog(svc.Name(), svc.Authenticate(), func(ses cchat.Session) {
container.AddSession(ses)
// Save all sessions.
for _, err := range keyring.SaveSessions(svc, container.Sessions()) {
log.Error(err)
}
// Try and save all keyring sessions.
app.SaveAllSessions(container)
})
}
func (app *App) SaveAllSessions(container *service.Container) {
keyring.SaveSessions(container.Service.Name(), container.KeyringSessions())
}
func (app *App) Header() gtk.IWidget {
return app.header
}