1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2024-12-23 12:46:45 +00:00
cchat-gtk/internal/ui/service/service.go
diamondburned (Forefront) f10aa71003 UI changes; typing state working
This commit refactors the input container's UI as well as fixing some
bugs related to asynchronous fetching of images.

It also adds complete typing indicator capabilities, all without using a
single mutex!
2020-07-03 21:41:12 -07:00

243 lines
5.8 KiB
Go

package service
import (
"fmt"
"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 View struct {
*gtk.ScrolledWindow
Box *gtk.Box
Services []*Container
}
var servicesCSS = primitives.PrepareCSS(`
.services {
background-color: @theme_base_color;
}
`)
func NewView() *View {
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.Show()
primitives.AddClass(box, "services")
primitives.AttachCSS(box, servicesCSS)
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
sw.Add(box)
return &View{
sw,
box,
nil,
}
}
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 {
// RowSelected is wrapped around session's MessageRowSelected.
RowSelected(*session.Row, *server.ServerRow, cchat.ServerMessage)
// AuthenticateSession is called to spawn the authentication dialog.
AuthenticateSession(*Container, cchat.Service)
// OnSessionRemove is called to remove a session. This should also clear out
// the message view in the parent package.
OnSessionRemove(id string)
// OnSessionDisconnect is here to satisfy session's controller.
OnSessionDisconnect(id string)
}
// Container represents a single service, including the button header and the
// child containers.
type Container struct {
*gtk.Box
Service cchat.Service
header *header
revealer *gtk.Revealer
children *children
// Embed controller and extend it to override RestoreSession.
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()
chrev, _ := gtk.RevealerNew()
chrev.SetRevealChild(true)
chrev.Add(children)
chrev.Show()
header := newHeader(svc)
header.SetActive(chrev.GetRevealChild())
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.Show()
box.PackStart(header, false, false, 0)
box.PackStart(chrev, false, false, 0)
primitives.AddClass(box, "service")
container := &Container{
Box: box,
Service: svc,
header: header,
revealer: chrev,
children: children,
Controller: ctrl,
}
// On click, toggle reveal.
header.Connect("clicked", func() {
revealed := !chrev.GetRevealChild()
chrev.SetRevealChild(revealed)
header.SetActive(revealed)
})
// On click, show the auth dialog.
header.Add.Connect("clicked", func() {
ctrl.AuthenticateSession(container, svc)
})
// Add more menu item(s).
header.Menu.AddSimpleItem("Save Sessions", container.SaveAllSessions)
return container
}
func (c *Container) GetService() cchat.Service {
return c.Service
}
func (c *Container) Sessions() []*session.Row {
return c.children.Sessions()
}
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, id, name, c)
c.children.AddSessionRow(id, srow)
return srow
}
func (c *Container) RemoveSession(row *session.Row) {
var id = row.Session.ID()
c.children.RemoveSessionRow(id)
c.SaveAllSessions()
// Call the parent's method.
c.Controller.OnSessionRemove(id)
}
func (c *Container) MoveSession(rowID, beneathRowID string) {
c.children.MoveSession(rowID, beneathRowID)
c.SaveAllSessions()
}
func (c *Container) OnSessionDisconnect(ses *session.Row) {
c.Controller.OnSessionDisconnect(ses.ID())
}
// RestoreSession tries to restore sessions asynchronously. This satisfies
// session.Controller.
func (c *Container) RestoreSession(row *session.Row, id string) {
// Can this session be restored? If not, exit.
restorer, ok := c.Service.(cchat.SessionRestorer)
if !ok {
return
}
// Do we even have a session stored?
krs := keyring.RestoreSession(c.Service.Name(), id)
if krs == nil {
log.Error(fmt.Errorf(
"Missing keyring for service %s, session ID %s",
c.Service.Name().Content, id,
))
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(err) })
} else {
gts.ExecAsync(func() { r.SetSession(s) })
}
}()
}
func (c *Container) SaveAllSessions() {
var sessions = c.children.Sessions()
var ksessions = make([]keyring.Session, 0, len(sessions))
for _, s := range sessions {
if k := s.KeyringSession(); k != nil {
ksessions = append(ksessions, *k)
}
}
keyring.SaveSessions(c.Service.Name(), ksessions)
}
func (c *Container) Breadcrumb() breadcrumb.Breadcrumb {
return breadcrumb.Try(nil, c.header.GetText())
}