mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-05-23 23:52:10 +00:00
More features, too many to list
This commit is contained in:
parent
43aa4dcef3
commit
8d9c3f2da5
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
f.sender = sender
|
if sender != nil {
|
||||||
f.text.SetSensitive(sender != nil) // grey if sender is nil
|
f.sender = sender
|
||||||
|
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)
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SendMessageData struct {
|
type SendMessageData struct {
|
||||||
content string
|
content string
|
||||||
author text.Rich
|
author text.Rich
|
||||||
authorID string
|
authorID string
|
||||||
nonce string
|
authorURL string // avatar
|
||||||
|
nonce string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PresendMessage interface {
|
type PresendMessage interface {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
|
||||||
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
|
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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -14,11 +14,12 @@ 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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
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"))
|
log.Error(errors.Wrap(err, "Failed to get servers"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return children
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Children) SetServers(servers []cchat.Server) {
|
func (c *Children) SetServers(servers []cchat.Server) {
|
||||||
|
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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())
|
|
||||||
|
|
||||||
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) RemoveSession(string) {
|
||||||
func (app *App) RestoreSession(row *session.Row, r cchat.SessionRestorer) {
|
app.window.MessageView.Reset()
|
||||||
// Get the restore data.
|
app.header.SetBreadcrumb(nil)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue