1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2025-05-24 16:11:04 +00:00

More features, too many to list

This commit is contained in:
diamondburned (Forefront) 2020-06-07 00:06:13 -07:00
parent 43aa4dcef3
commit 8d9c3f2da5
15 changed files with 310 additions and 159 deletions

View file

@ -5,6 +5,7 @@ import (
"encoding/gob" "encoding/gob"
"strings" "strings"
"github.com/diamondburned/cchat"
"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"
@ -37,6 +38,31 @@ type Session struct {
Data map[string]string Data map[string]string
} }
func GetSession(ses cchat.Session, name string) *Session {
saver, ok := ses.(cchat.SessionSaver)
if !ok {
return nil
}
s, err := saver.Save()
if err != nil {
log.Error(errors.Wrapf(err, "Failed to save session ID %s (%s)", ses.ID(), name))
return nil
}
// Treat the ID as name if none is provided. This is a shitty hack around
// backends that only set the name after returning.
if name == "" {
name = ses.ID()
}
return &Session{
ID: ses.ID(),
Name: name,
Data: s,
}
}
func SaveSessions(serviceName string, sessions []Session) { func SaveSessions(serviceName string, sessions []Session) {
if err := set(serviceName, sessions); err != nil { if err := set(serviceName, sessions); err != nil {
log.Warn(errors.Wrap(err, "Error saving session")) log.Warn(errors.Wrap(err, "Error saving session"))

View file

@ -40,7 +40,12 @@ func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage { func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
var presend = NewFullSendingMessage(msg) var presend = NewFullSendingMessage(msg)
c.reuseAvatar(msg.AuthorID(), presend.Avatar)
// Try and see if we can reuse the avatar, and fallback if possible.
if !c.reuseAvatar(msg.AuthorID(), presend.Avatar) {
presend.overrideAuthorAvatar(msg.AuthorAvatarURL())
}
return presend return presend
} }

View file

@ -114,3 +114,18 @@ func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage {
FullMessage: *WrapFullMessage(msgc.GenericContainer), FullMessage: *WrapFullMessage(msgc.GenericContainer),
} }
} }
// make an exception for sending messages.
func (m *FullSendingMessage) overrideAuthorAvatar(url string) {
if url == "" {
return
}
// TODO: put in fn
httputil.AsyncImageSized(
m.Avatar,
url,
AvatarSize, AvatarSize,
imgutil.Round(true),
)
}

View file

@ -1,6 +1,9 @@
package input package input
import ( import (
"strconv"
"time"
"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"
@ -71,20 +74,30 @@ func NewField(ctrl Controller) *Field {
return field return field
} }
// SetSender changes the sender of the input field. If nil, the input will be // Reset prepares the field before SetSender() is called.
// disabled. func (f *Field) Reset() {
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) { // Paranoia.
f.UserID = session.ID() f.text.SetSensitive(false)
f.UserID = ""
f.sender = nil
f.username.Reset()
// reset the input
f.buffer.Delete(f.buffer.GetBounds())
}
// SetSender changes the sender of the input field. If nil, the input will be
// disabled. Reset() should be called first.
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
// Update the left username container in the input. // Update the left username container in the input.
f.username.Update(session, sender) f.username.Update(session, sender)
// Set the sender. // Set the sender.
if sender != nil {
f.sender = sender f.sender = sender
f.text.SetSensitive(sender != nil) // grey if sender is nil f.text.SetSensitive(true)
}
// reset the input
f.buffer.Delete(f.buffer.GetBounds())
} }
// SendMessage yanks the text from the input field and sends it to the backend. // SendMessage yanks the text from the input field and sends it to the backend.
@ -100,7 +113,13 @@ func (f *Field) SendMessage() {
} }
var sender = f.sender var sender = f.sender
var data = NewSendMessageData(text, f.username.GetLabel(), f.UserID) var data = SendMessageData{
content: text,
author: f.username.GetLabel(),
authorID: f.UserID,
authorURL: f.username.GetIconURL(),
nonce: "cchat-gtk_" + strconv.FormatInt(time.Now().UnixNano(), 10),
}
// presend message into the container through the controller // presend message into the container through the controller
var done = f.ctrl.PresendMessage(data) var done = f.ctrl.PresendMessage(data)

View file

@ -1,9 +1,6 @@
package input package input
import ( import (
"strconv"
"time"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
) )
@ -12,6 +9,7 @@ type SendMessageData struct {
content string content string
author text.Rich author text.Rich
authorID string authorID string
authorURL string // avatar
nonce string nonce string
} }
@ -21,6 +19,7 @@ type PresendMessage interface {
Author() text.Rich Author() text.Rich
AuthorID() string AuthorID() string
AuthorAvatarURL() string // may be empty
} }
var ( var (
@ -28,14 +27,6 @@ var (
_ cchat.MessageNonce = (*SendMessageData)(nil) _ cchat.MessageNonce = (*SendMessageData)(nil)
) )
func NewSendMessageData(content string, author text.Rich, authorID string) SendMessageData {
return SendMessageData{
content: content,
author: author,
nonce: "cchat-gtk_" + strconv.FormatInt(time.Now().UnixNano(), 10),
}
}
func (s SendMessageData) Content() string { func (s SendMessageData) Content() string {
return s.content return s.content
} }
@ -48,6 +39,10 @@ func (s SendMessageData) AuthorID() string {
return s.authorID return s.authorID
} }
func (s SendMessageData) AuthorAvatarURL() string {
return s.authorURL
}
func (s SendMessageData) Nonce() string { func (s SendMessageData) Nonce() string {
return s.nonce return s.nonce
} }

View file

@ -60,6 +60,12 @@ func newUsernameContainer() *usernameContainer {
} }
} }
func (u *usernameContainer) Reset() {
u.SetRevealChild(false)
u.avatar.Reset()
u.label.Reset()
}
// Update is not thread-safe. // Update is not thread-safe.
func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMessageSender) { func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMessageSender) {
// Does sender (aka Server) implement ServerNickname? If not, we fallback to // Does sender (aka Server) implement ServerNickname? If not, we fallback to
@ -113,3 +119,8 @@ func (u *usernameContainer) SetIcon(url string) {
} }
}) })
} }
// GetIconURL is not thread-safe.
func (u *usernameContainer) GetIconURL() string {
return u.avatar.URL()
}

View file

@ -46,8 +46,8 @@ func NewView() *View {
return view return view
} }
// JoinServer is not thread-safe, but it calls backend functions asynchronously. func (v *View) Reset() {
func (v *View) JoinServer(session cchat.Session, server cchat.ServerMessage) { // Leave the server if any.
if v.current != nil { if v.current != nil {
// Backend should handle synchronizing joins and leaves if it needs to. // Backend should handle synchronizing joins and leaves if it needs to.
go func() { go func() {
@ -55,11 +55,19 @@ func (v *View) JoinServer(session cchat.Session, server cchat.ServerMessage) {
log.Error(errors.Wrap(err, "Error leaving server")) log.Error(errors.Wrap(err, "Error leaving server"))
} }
}() }()
}
// Clean all messages. // Clean all messages.
v.Container.Reset() v.Container.Reset()
// Reset the input.
v.SendInput.Reset()
} }
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
func (v *View) JoinServer(session cchat.Session, server cchat.ServerMessage) {
// Reset before setting.
v.Reset()
v.current = server v.current = server
// Skipping ok check because sender can be nil. Without the empty check, Go // Skipping ok check because sender can be nil. Without the empty check, Go

View file

@ -1,6 +1,11 @@
package primitives package primitives
import "github.com/gotk3/gotk3/gtk" import (
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
)
type StyleContexter interface { type StyleContexter interface {
GetStyleContext() (*gtk.StyleContext, error) GetStyleContext() (*gtk.StyleContext, error)
@ -41,20 +46,32 @@ func SetImageIcon(img *gtk.Image, icon string, sizepx int) {
img.SetSizeRequest(sizepx, sizepx) img.SetSizeRequest(sizepx, sizepx)
} }
type MenuItem struct { func AppendMenuItems(menu interface{ Append(gtk.IMenuItem) }, items []*gtk.MenuItem) {
Name string
Fn func()
}
func AppendMenuItems(menu interface{ Append(gtk.IMenuItem) }, items []MenuItem) {
for _, item := range items { for _, item := range items {
menu.Append(NewMenuItem(item.Name, item.Fn)) menu.Append(item)
} }
} }
func NewMenuItem(label string, fn func()) *gtk.MenuItem { func HiddenMenuItem(label string, fn func()) *gtk.MenuItem {
mb, _ := gtk.MenuItemNewWithLabel(label) mb, _ := gtk.MenuItemNewWithLabel(label)
mb.Show()
mb.Connect("activate", fn) mb.Connect("activate", fn)
return mb return mb
} }
func MenuItem(label string, fn func()) *gtk.MenuItem {
menuitem := HiddenMenuItem(label, fn)
menuitem.Show()
return menuitem
}
type Connector interface {
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
}
func BindMenu(menu *gtk.Menu, connector Connector) {
connector.Connect("event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
if gts.EventIsRightClick(ev) {
menu.PopupAtPointer(ev)
}
})
}

View file

@ -15,10 +15,11 @@ import (
type Icon struct { type Icon struct {
*gtk.Revealer *gtk.Revealer
Image *gtk.Image Image *gtk.Image
resizer imgutil.Processor resizer imgutil.Processor
procs []imgutil.Processor procs []imgutil.Processor
url string // state
// state
url string
} }
const DefaultIconSize = 16 const DefaultIconSize = 16
@ -48,6 +49,12 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
} }
} }
// Reset wipes the state to be just after construction.
func (i *Icon) Reset() {
i.url = ""
i.Revealer.SetRevealChild(false)
}
// URL is not thread-safe. // URL is not thread-safe.
func (i *Icon) URL() string { func (i *Icon) URL() string {
return i.url return i.url

View file

@ -44,6 +44,12 @@ func NewLabel(content text.Rich) *Label {
return &Label{*label, content} return &Label{*label, content}
} }
// Reset wipes the state to be just after construction.
func (l *Label) Reset() {
l.current = text.Rich{}
l.Label.SetText("")
}
// SetLabel is thread-safe. // SetLabel is thread-safe.
func (l *Label) SetLabel(content text.Rich) { func (l *Label) SetLabel(content text.Rich) {
gts.ExecAsync(func() { gts.ExecAsync(func() {

View file

@ -2,12 +2,11 @@ package service
import ( import (
"github.com/diamondburned/cchat" "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/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/text" "github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil" "github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -47,14 +46,9 @@ func newHeader(svc cchat.Service) *header {
} }
} }
menu, _ := gtk.MenuNew()
// Spawn the menu on right click. // Spawn the menu on right click.
reveal.Connect("event", func(_ *gtk.ToggleButton, ev *gdk.Event) { menu, _ := gtk.MenuNew()
if gts.EventIsRightClick(ev) { primitives.BindMenu(menu, reveal)
menu.PopupAtPointer(ev)
}
})
return &header{box, reveal, add, menu} return &header{box, reveal, add, menu}
} }

View file

@ -2,25 +2,17 @@ package service
import ( import (
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/keyring" "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/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-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/pkg/errors"
) )
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 { type View struct {
*gtk.ScrolledWindow *gtk.ScrolledWindow
Box *gtk.Box Box *gtk.Box
@ -49,9 +41,23 @@ func (v *View) AddService(svc cchat.Service, ctrl Controller) *Container {
s := NewContainer(svc, ctrl) s := NewContainer(svc, ctrl)
v.Services = append(v.Services, s) v.Services = append(v.Services, s)
v.Box.Add(s) v.Box.Add(s)
// Try and restore all sessions.
s.restoreAllSessions()
return s return s
} }
type Controller interface {
// 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)
// RemoveSession is called to remove a session. This should also clear out
// the message view in the parent package.
RemoveSession(id string)
}
type Container struct { type Container struct {
*gtk.Box *gtk.Box
Service cchat.Service Service cchat.Service
@ -64,6 +70,9 @@ type Container struct {
Controller Controller
} }
// Guarantee that our interface is up-to-date with session's controller.
var _ session.Controller = (*Container)(nil)
func NewContainer(svc cchat.Service, ctrl Controller) *Container { func NewContainer(svc cchat.Service, ctrl Controller) *Container {
children := newChildren() children := newChildren()
@ -104,10 +113,10 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
}) })
// Make menu items. // Make menu items.
primitives.AppendMenuItems(header.Menu, []primitives.MenuItem{ primitives.AppendMenuItems(header.Menu, []*gtk.MenuItem{
{Name: "Save Sessions", Fn: func() { primitives.MenuItem("Save Sessions", func() {
ctrl.SaveAllSessions(container) container.SaveAllSessions()
}}, }),
}) })
return container return container
@ -116,20 +125,74 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
func (c *Container) AddSession(ses cchat.Session) *session.Row { func (c *Container) AddSession(ses cchat.Session) *session.Row {
srow := session.New(c, ses, c) srow := session.New(c, ses, c)
c.children.addSessionRow(ses.ID(), srow) c.children.addSessionRow(ses.ID(), srow)
c.SaveAllSessions()
return srow return srow
} }
func (c *Container) AddLoadingSession(id, name string) *session.Row { func (c *Container) AddLoadingSession(id, name string) *session.Row {
srow := session.NewLoading(c, name, c) srow := session.NewLoading(c, name, c)
c.children.addSessionRow(id, srow) c.children.addSessionRow(id, srow)
return srow return srow
} }
// KeyringSessions returns all known keyring sessions. Sessions that can't be func (c *Container) RemoveSession(id string) {
c.children.removeSessionRow(id)
c.SaveAllSessions()
// Call the parent's method.
c.Controller.RemoveSession(id)
}
// RestoreSession tries to restore sessions asynchronously. This satisfies
// session.Controller.
func (c *Container) RestoreSession(row *session.Row, krs keyring.Session) {
// Can this session be restored? If not, exit.
restorer, ok := c.Service.(cchat.SessionRestorer)
if !ok {
return
}
c.restoreSession(row, restorer, krs)
}
// internal method called on AddService.
func (c *Container) restoreAllSessions() {
// Can this session be restored? If not, exit.
restorer, ok := c.Service.(cchat.SessionRestorer)
if !ok {
return
}
var sessions = keyring.RestoreSessions(c.Service.Name())
for _, krs := range sessions {
// Copy the session to avoid race conditions.
krs := krs
row := c.AddLoadingSession(krs.ID, krs.Name)
c.restoreSession(row, restorer, krs)
}
}
func (c *Container) restoreSession(r *session.Row, res cchat.SessionRestorer, k keyring.Session) {
go func() {
s, err := res.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() { r.SetFailed(k, err) })
} else {
gts.ExecAsync(func() { r.SetSession(s) })
}
}()
}
func (c *Container) SaveAllSessions() {
keyring.SaveSessions(c.Service.Name(), c.keyringSessions())
}
// keyringSessions returns all known keyring sessions. Sessions that can't be
// saved will not be in the slice. // saved will not be in the slice.
func (c *Container) KeyringSessions() []keyring.Session { func (c *Container) keyringSessions() []keyring.Session {
var ksessions = make([]keyring.Session, 0, len(c.children.Sessions)) var ksessions = make([]keyring.Session, 0, len(c.children.Sessions))
for _, s := range c.children.Sessions { for _, s := range c.children.Sessions {
if k := s.KeyringSession(); k != nil { if k := s.KeyringSession(); k != nil {

View file

@ -62,9 +62,10 @@ func NewRow(parent breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller
switch server := server.(type) { switch server := server.(type) {
case cchat.ServerList: case cchat.ServerList:
row.children = NewChildren(row, server, ctrl) row.children = NewChildren(row, ctrl)
box.PackStart(row.children, false, false, 0) row.children.SetServerList(server)
box.PackStart(row.children, false, false, 0)
primitives.AddClass(box, "server-list") primitives.AddClass(box, "server-list")
case cchat.ServerMessage: case cchat.ServerMessage:
@ -112,7 +113,7 @@ type Children struct {
Parent breadcrumb.Breadcrumber Parent breadcrumb.Breadcrumber
} }
func NewChildren(parent breadcrumb.Breadcrumber, list cchat.ServerList, ctrl Controller) *Children { func NewChildren(parent breadcrumb.Breadcrumber, ctrl Controller) *Children {
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
main.SetMarginStart(ChildrenMargin) main.SetMarginStart(ChildrenMargin)
main.Show() main.Show()
@ -122,19 +123,20 @@ func NewChildren(parent breadcrumb.Breadcrumber, list cchat.ServerList, ctrl Con
rev.Add(main) rev.Add(main)
rev.Show() rev.Show()
children := &Children{ return &Children{
Revealer: rev, Revealer: rev,
Main: main, Main: main,
List: list,
rowctrl: ctrl, rowctrl: ctrl,
Parent: parent, Parent: parent,
} }
if err := list.Servers(children); err != nil {
log.Error(errors.Wrap(err, "Failed to get servers"))
} }
return children func (c *Children) SetServerList(list cchat.ServerList) {
c.List = list
if err := list.Servers(c); err != nil {
log.Error(errors.Wrap(err, "Failed to get servers"))
}
} }
func (c *Children) SetServers(servers []cchat.Server) { func (c *Children) SetServers(servers []cchat.Server) {

View file

@ -3,7 +3,6 @@ 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/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"
@ -11,7 +10,6 @@ import (
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil" "github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
) )
const IconSize = 32 const IconSize = 32
@ -19,18 +17,24 @@ 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 RestoreSession(*Row, keyring.Session) // async
RemoveSession(id string)
} }
type Row struct { type Row struct {
*gtk.Box *gtk.Box
Button *rich.ToggleButtonImage Button *rich.ToggleButtonImage
Session cchat.Session Session cchat.Session
Servers *server.Children Servers *server.Children
menu *gtk.Menu
retry *gtk.MenuItem
ctrl Controller ctrl Controller
parent breadcrumb.Breadcrumber parent breadcrumb.Breadcrumber
// nil after calling SetSession()
krs keyring.Session
} }
func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Row { func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Row {
@ -42,8 +46,7 @@ func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Ro
func NewLoading(parent breadcrumb.Breadcrumber, name string, ctrl Controller) *Row { func NewLoading(parent breadcrumb.Breadcrumber, name string, ctrl Controller) *Row {
row := new(parent, ctrl) row := new(parent, ctrl)
row.Button.SetLabelUnsafe(text.Rich{Content: name}) row.Button.SetLabelUnsafe(text.Rich{Content: name})
row.Button.Image.SetPlaceholderIcon("content-loading-symbolic", IconSize) row.setLoading()
row.SetSensitive(false)
return row return row
} }
@ -53,6 +56,7 @@ func new(parent breadcrumb.Breadcrumber, ctrl Controller) *Row {
ctrl: ctrl, ctrl: ctrl,
parent: parent, parent: parent,
} }
row.Servers = server.NewChildren(parent, 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)
@ -74,48 +78,72 @@ func new(parent breadcrumb.Breadcrumber, ctrl Controller) *Row {
primitives.AddClass(row.Box, "session") primitives.AddClass(row.Box, "session")
row.menu, _ = gtk.MenuNew()
primitives.BindMenu(row.menu, row.Button)
row.retry = primitives.HiddenMenuItem("Retry", func() {
// Show the loading stuff.
row.setLoading()
// Reuse the failed keyring session provided. As this variable is reset
// after a success, it relies of the button not triggering.
ctrl.RestoreSession(row, row.krs)
})
row.retry.SetSensitive(false)
primitives.AppendMenuItems(row.menu, []*gtk.MenuItem{
row.retry,
primitives.MenuItem("Remove", func() {
ctrl.RemoveSession(row.Session.ID())
}),
})
return row return row
} }
func (r *Row) setLoading() {
// set the loading icon
r.Button.Image.SetPlaceholderIcon("content-loading-symbolic", IconSize)
// restore the old label's color
r.Button.SetLabelUnsafe(r.Button.GetLabel())
// blur - set the color darker
r.SetSensitive(false)
}
// KeyringSession returns a keyring session, or nil if the session cannot be // 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. // saved.
func (r *Row) KeyringSession() *keyring.Session { func (r *Row) KeyringSession() *keyring.Session {
// Is the session saveable? return keyring.GetSession(r.Session, r.Button.GetText())
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) { func (r *Row) SetSession(ses cchat.Session) {
// Disable the retry button.
r.retry.SetSensitive(false)
r.retry.Hide()
r.Session = ses r.Session = ses
r.Servers = server.NewChildren(r, ses, r) r.Servers.SetServerList(ses)
r.Button.Image.SetPlaceholderIcon("user-available-symbolic", IconSize) r.Button.Image.SetPlaceholderIcon("user-available-symbolic", IconSize)
r.Box.PackStart(r.Servers, false, false, 0) r.Box.PackStart(r.Servers, false, false, 0)
r.SetSensitive(true) r.SetSensitive(true)
// Set the session's name to the button. // Set the session's name to the button.
r.Button.Try(ses, "session") r.Button.Try(ses, "session")
// Wipe the keyring session off.
r.krs = keyring.Session{}
} }
func (r *Row) SetFailed(err error) { func (r *Row) SetFailed(krs keyring.Session, err error) {
// Set the failed keyring session.
r.krs = krs
// Allow the retry button to be pressed.
r.retry.SetSensitive(true)
r.retry.Show()
r.SetSensitive(true)
r.SetTooltipText(err.Error()) r.SetTooltipText(err.Error())
// TODO: setting the label directly here is kind of shitty, as it screws up // Intentional side-effect of not changing the actual label state.
// the getter. Fix?
r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel())) r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel()))
} }

View file

@ -3,15 +3,12 @@ package ui
import ( 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/keyring"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/service" "github.com/diamondburned/cchat-gtk/internal/ui/service"
"github.com/diamondburned/cchat-gtk/internal/ui/service/auth" "github.com/diamondburned/cchat-gtk/internal/ui/service/auth"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session" "github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"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() {
@ -45,47 +42,12 @@ 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) app.window.Services.AddService(svc, app)
// Can this session be restored? If not, exit.
restorer, ok := container.Service.(cchat.SessionRestorer)
if !ok {
return
} }
var sessions = keyring.RestoreSessions(container.Service.Name()) func (app *App) RemoveSession(string) {
app.window.MessageView.Reset()
for _, krs := range sessions { app.header.SetBreadcrumb(nil)
// 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) {
@ -107,16 +69,9 @@ func (app *App) MessageRowSelected(ses *session.Row, srv *server.Row, smsg cchat
func (app *App) AuthenticateSession(container *service.Container, svc cchat.Service) { func (app *App) AuthenticateSession(container *service.Container, svc cchat.Service) {
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)
// 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 { func (app *App) Header() gtk.IWidget {
return app.header return app.header
} }