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"
"strings"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/pkg/errors"
"github.com/zalando/go-keyring"
@ -37,6 +38,31 @@ type Session struct {
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) {
if err := set(serviceName, sessions); err != nil {
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 {
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
}

View File

@ -114,3 +114,18 @@ func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage {
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
import (
"strconv"
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
@ -71,20 +74,30 @@ func NewField(ctrl Controller) *Field {
return field
}
// SetSender changes the sender of the input field. If nil, the input will be
// disabled.
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
f.UserID = session.ID()
// Reset prepares the field before SetSender() is called.
func (f *Field) Reset() {
// Paranoia.
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.
f.username.Update(session, sender)
// Set the sender.
f.sender = sender
f.text.SetSensitive(sender != nil) // grey if sender is nil
// reset the input
f.buffer.Delete(f.buffer.GetBounds())
if sender != nil {
f.sender = sender
f.text.SetSensitive(true)
}
}
// 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 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
var done = f.ctrl.PresendMessage(data)

View File

@ -1,18 +1,16 @@
package input
import (
"strconv"
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat/text"
)
type SendMessageData struct {
content string
author text.Rich
authorID string
nonce string
content string
author text.Rich
authorID string
authorURL string // avatar
nonce string
}
type PresendMessage interface {
@ -21,6 +19,7 @@ type PresendMessage interface {
Author() text.Rich
AuthorID() string
AuthorAvatarURL() string // may be empty
}
var (
@ -28,14 +27,6 @@ var (
_ 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 {
return s.content
}
@ -48,6 +39,10 @@ func (s SendMessageData) AuthorID() string {
return s.authorID
}
func (s SendMessageData) AuthorAvatarURL() string {
return s.authorURL
}
func (s SendMessageData) Nonce() string {
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.
func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMessageSender) {
// 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
}
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
func (v *View) JoinServer(session cchat.Session, server cchat.ServerMessage) {
func (v *View) Reset() {
// Leave the server if any.
if v.current != nil {
// Backend should handle synchronizing joins and leaves if it needs to.
go func() {
@ -55,11 +55,19 @@ func (v *View) JoinServer(session cchat.Session, server cchat.ServerMessage) {
log.Error(errors.Wrap(err, "Error leaving server"))
}
}()
// Clean all messages.
v.Container.Reset()
}
// Clean all messages.
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
// Skipping ok check because sender can be nil. Without the empty check, Go

View File

@ -1,6 +1,11 @@
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 {
GetStyleContext() (*gtk.StyleContext, error)
@ -41,20 +46,32 @@ func SetImageIcon(img *gtk.Image, icon string, sizepx int) {
img.SetSizeRequest(sizepx, sizepx)
}
type MenuItem struct {
Name string
Fn func()
}
func AppendMenuItems(menu interface{ Append(gtk.IMenuItem) }, items []MenuItem) {
func AppendMenuItems(menu interface{ Append(gtk.IMenuItem) }, items []*gtk.MenuItem) {
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.Show()
mb.Connect("activate", fn)
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

@ -14,11 +14,12 @@ import (
type Icon struct {
*gtk.Revealer
Image *gtk.Image
Image *gtk.Image
resizer imgutil.Processor
procs []imgutil.Processor
url string // state
// state
url string
}
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.
func (i *Icon) URL() string {
return i.url

View File

@ -44,6 +44,12 @@ func NewLabel(content text.Rich) *Label {
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.
func (l *Label) SetLabel(content text.Rich) {
gts.ExecAsync(func() {

View File

@ -2,12 +2,11 @@ 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/primitives"
"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"
)
@ -47,14 +46,9 @@ func newHeader(svc cchat.Service) *header {
}
}
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)
}
})
menu, _ := gtk.MenuNew()
primitives.BindMenu(menu, reveal)
return &header{box, reveal, add, menu}
}

View File

@ -2,25 +2,17 @@ package service
import (
"github.com/diamondburned/cchat"
"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/primitives"
"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/server"
"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 {
*gtk.ScrolledWindow
Box *gtk.Box
@ -49,9 +41,23 @@ func (v *View) AddService(svc cchat.Service, ctrl Controller) *Container {
s := NewContainer(svc, ctrl)
v.Services = append(v.Services, s)
v.Box.Add(s)
// Try and restore all sessions.
s.restoreAllSessions()
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 {
*gtk.Box
Service cchat.Service
@ -64,6 +70,9 @@ type Container struct {
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 {
children := newChildren()
@ -104,10 +113,10 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
})
// Make menu items.
primitives.AppendMenuItems(header.Menu, []primitives.MenuItem{
{Name: "Save Sessions", Fn: func() {
ctrl.SaveAllSessions(container)
}},
primitives.AppendMenuItems(header.Menu, []*gtk.MenuItem{
primitives.MenuItem("Save Sessions", func() {
container.SaveAllSessions()
}),
})
return container
@ -116,20 +125,74 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
func (c *Container) AddSession(ses cchat.Session) *session.Row {
srow := session.New(c, ses, c)
c.children.addSessionRow(ses.ID(), srow)
c.SaveAllSessions()
return srow
}
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
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.
func (c *Container) KeyringSessions() []keyring.Session {
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 {

View File

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

View File

@ -3,7 +3,6 @@ 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"
@ -11,7 +10,6 @@ import (
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const IconSize = 32
@ -19,18 +17,24 @@ const IconSize = 32
// Controller extends server.RowController to add session.
type Controller interface {
MessageRowSelected(*Row, *server.Row, cchat.ServerMessage)
RestoreSession(*Row, cchat.SessionRestorer) // async
RestoreSession(*Row, keyring.Session) // async
RemoveSession(id string)
}
type Row struct {
*gtk.Box
Button *rich.ToggleButtonImage
Session cchat.Session
Servers *server.Children
menu *gtk.Menu
retry *gtk.MenuItem
ctrl Controller
parent breadcrumb.Breadcrumber
// nil after calling SetSession()
krs keyring.Session
}
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 {
row := new(parent, ctrl)
row.Button.SetLabelUnsafe(text.Rich{Content: name})
row.Button.Image.SetPlaceholderIcon("content-loading-symbolic", IconSize)
row.SetSensitive(false)
row.setLoading()
return row
}
@ -53,6 +56,7 @@ func new(parent breadcrumb.Breadcrumber, ctrl Controller) *Row {
ctrl: ctrl,
parent: parent,
}
row.Servers = server.NewChildren(parent, row)
row.Button = rich.NewToggleButtonImage(text.Rich{})
row.Button.Box.SetHAlign(gtk.ALIGN_START)
@ -74,48 +78,72 @@ func new(parent breadcrumb.Breadcrumber, ctrl Controller) *Row {
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
}
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
// saved. This function is not cached, as I'd rather not keep the map in memory.
// saved.
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
return keyring.GetSession(r.Session, r.Button.GetText())
}
func (r *Row) SetSession(ses cchat.Session) {
// Disable the retry button.
r.retry.SetSensitive(false)
r.retry.Hide()
r.Session = ses
r.Servers = server.NewChildren(r, ses, r)
r.Servers.SetServerList(ses)
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")
// 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())
// TODO: setting the label directly here is kind of shitty, as it screws up
// the getter. Fix?
// Intentional side-effect of not changing the actual label state.
r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel()))
}

View File

@ -3,15 +3,12 @@ package ui
import (
"github.com/diamondburned/cchat"
"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/auth"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"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() {
@ -45,47 +42,12 @@ func NewApplication() *App {
}
func (app *App) AddService(svc cchat.Service) {
var container = 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())
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)
}
app.window.Services.AddService(svc, app)
}
// 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) RemoveSession(string) {
app.window.MessageView.Reset()
app.header.SetBreadcrumb(nil)
}
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) {
auth.NewDialog(svc.Name(), svc.Authenticate(), func(ses cchat.Session) {
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 {
return app.header
}