cchat-gtk/internal/ui/messages/view.go

297 lines
7.6 KiB
Go
Raw Normal View History

2020-06-06 07:44:36 +00:00
package messages
2020-05-26 06:51:06 +00:00
import (
"context"
2020-05-26 06:51:06 +00:00
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/icons"
"github.com/diamondburned/cchat-gtk/internal/gts"
2020-05-26 06:51:06 +00:00
"github.com/diamondburned/cchat-gtk/internal/log"
2020-06-20 04:40:34 +00:00
"github.com/diamondburned/cchat-gtk/internal/ui/config"
2020-06-06 07:44:36 +00:00
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
2020-06-20 04:40:34 +00:00
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/compact"
2020-06-07 04:27:28 +00:00
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/cozy"
2020-06-06 07:44:36 +00:00
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/sadface"
2020-07-03 03:22:48 +00:00
"github.com/diamondburned/cchat-gtk/internal/ui/messages/typing"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
2020-05-26 06:51:06 +00:00
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
2020-06-20 04:40:34 +00:00
const (
cozyMessage int = iota
compactMessage
)
2020-06-04 23:00:41 +00:00
2020-06-20 04:40:34 +00:00
var msgIndex = cozyMessage
2020-06-04 23:00:41 +00:00
2020-06-20 04:40:34 +00:00
func init() {
config.AppearanceAdd("Message Display", config.Combo(
&msgIndex, // 0 or 1
[]string{"Cozy", "Compact"},
nil,
))
2020-06-04 23:00:41 +00:00
}
2020-05-26 06:51:06 +00:00
type View struct {
*sadface.FaceView
Box *gtk.Box
2020-07-03 03:22:48 +00:00
Scroller *autoscroll.ScrolledWindow
InputView *input.InputView
2020-07-03 03:22:48 +00:00
MsgBox *gtk.Box
Typing *typing.Container
2020-06-06 07:44:36 +00:00
Container container.Container
2020-06-20 04:40:34 +00:00
contType int // msgIndex
2020-05-26 06:51:06 +00:00
// Inherit some useful methods.
state
2020-05-26 06:51:06 +00:00
}
func NewView() *View {
2020-06-04 23:00:41 +00:00
view := &View{}
2020-07-03 03:22:48 +00:00
view.Typing = typing.New()
view.Typing.Show()
2020-06-04 23:00:41 +00:00
2020-07-03 03:22:48 +00:00
view.MsgBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
view.MsgBox.PackEnd(view.Typing, false, false, 0)
view.MsgBox.Show()
2020-06-04 23:00:41 +00:00
2020-06-20 04:40:34 +00:00
// Create the message container, which will use PackEnd to add the widget on
2020-07-03 03:22:48 +00:00
// TOP of the typing indicator.
2020-06-20 04:40:34 +00:00
view.createMessageContainer()
2020-07-03 03:22:48 +00:00
view.Scroller = autoscroll.NewScrolledWindow()
view.Scroller.Add(view.MsgBox)
view.Scroller.Show()
// A separator to go inbetween.
sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
sep.Show()
view.InputView = input.NewView(view)
view.InputView.Show()
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
view.Box.PackStart(view.Scroller, true, true, 0)
view.Box.PackStart(sep, false, false, 0)
view.Box.PackStart(view.InputView, false, false, 0)
view.Box.Show()
// placeholder logo
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256())
logo.Show()
view.FaceView = sadface.New(view.Box, logo)
2020-06-04 23:00:41 +00:00
return view
2020-05-26 06:51:06 +00:00
}
2020-06-20 04:40:34 +00:00
func (v *View) createMessageContainer() {
// Remove the old message container.
if v.Container != nil {
2020-07-03 03:22:48 +00:00
v.MsgBox.Remove(v.Container)
2020-06-20 04:40:34 +00:00
}
// Update the container type.
switch v.contType = msgIndex; msgIndex {
case cozyMessage:
v.Container = cozy.NewContainer(v)
case compactMessage:
v.Container = compact.NewContainer(v)
}
// Add the new message container.
2020-07-03 03:22:48 +00:00
v.MsgBox.PackEnd(v.Container, true, true, 0)
2020-06-20 04:40:34 +00:00
}
2020-07-03 03:22:48 +00:00
func (v *View) Bottomed() bool { return v.Scroller.Bottomed }
2020-06-07 07:06:13 +00:00
func (v *View) Reset() {
v.state.Reset() // Reset the state variables.
v.FaceView.Reset() // Switch back to the main screen.
v.InputView.Reset() // Reset the input.
2020-06-20 04:40:34 +00:00
v.Container.Reset() // Clean all messages.
2020-07-03 03:22:48 +00:00
// Keep the scroller at the bottom.
v.Scroller.Bottomed = true
2020-06-20 04:40:34 +00:00
// Recreate the message container if the type is different.
if v.contType != msgIndex {
v.createMessageContainer()
}
2020-06-07 07:06:13 +00:00
}
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func()) {
2020-06-07 07:06:13 +00:00
// Reset before setting.
v.Reset()
2020-05-26 06:51:06 +00:00
// Set the screen to loading.
v.FaceView.SetLoading()
// Bind the state.
v.state.bind(session, server)
2020-06-28 23:01:08 +00:00
// Skipping ok check because sender can be nil. Without the empty
// check, Go will panic.
sender, _ := server.(cchat.ServerMessageSender)
// We're setting this variable before actually calling JoinServer. This is
// because new messages created by JoinServer will use this state for things
// such as determinining if it's deletable or not.
v.InputView.SetSender(session, sender)
gts.Async(func() (func(), error) {
// We can use a background context here, as the user can't go anywhere
// that would require cancellation anyway. This is done in ui.go.
s, err := server.JoinServer(context.Background(), v.Container)
if err != nil {
err = errors.Wrap(err, "Failed to join server")
// Even if we're erroring out, we're running the done() callback
// anyway.
return func() { done(); v.SetError(err) }, err
}
return func() {
// Run the done() callback.
done()
// Set the screen to the main one.
v.FaceView.SetMain()
// Set the cancel handler.
v.state.setcurrent(s)
}, nil
})
}
func (v *View) AddPresendMessage(msg input.PresendMessage) func(error) {
var presend = v.Container.AddPresendMessage(msg)
return func(err error) {
// Set the retry message.
presend.SetSentError(err)
// Only attach the menu once. Further retries do not need to be
// reattached.
presend.AttachMenu([]menu.Item{
menu.SimpleItem("Retry", func() {
presend.SetLoading()
v.retryMessage(msg, presend)
}),
})
}
}
// LatestMessageFrom returns the last message ID with that author.
func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) {
return v.Container.LatestMessageFrom(userID)
}
// retryMessage sends the message.
func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendGridMessage) {
var sender = v.InputView.Sender
if sender == nil {
return
}
2020-05-26 06:51:06 +00:00
2020-06-04 23:00:41 +00:00
go func() {
if err := sender.SendMessage(msg); err != nil {
// Set the message's state to errored again, but we don't need to
// rebind the menu.
gts.ExecAsync(func() { presend.SetSentError(err) })
2020-06-04 23:00:41 +00:00
}
}()
}
// BindMenu attaches the menu constructor into the message with the needed
// states and callbacks.
func (v *View) BindMenu(msg container.GridMessage) {
// Add 1 for the edit menu item.
2020-06-26 04:11:31 +00:00
var mitems []menu.Item
// Do we have editing capabilities? If yes, append a button to allow it.
2020-06-28 23:01:08 +00:00
if v.InputView.Editable(msg.ID()) {
mitems = append(mitems, menu.SimpleItem(
"Edit", func() { v.InputView.StartEditing(msg.ID()) },
))
}
// Do we have any custom actions? If yes, append it.
2020-06-26 04:11:31 +00:00
if v.hasActions() {
var actions = v.actioner.MessageActions(msg.ID())
var items = make([]menu.Item, len(actions))
for i, action := range actions {
items[i] = v.makeActionItem(action, msg.ID())
}
mitems = append(mitems, items...)
}
msg.AttachMenu(mitems)
}
// makeActionItem creates a new menu callback that's called on menu item
// activation.
func (v *View) makeActionItem(action, msgID string) menu.Item {
return menu.SimpleItem(action, func() {
go func() {
// Run, get the error, and try to log it. The logger will ignore nil
// errors.
err := v.state.actioner.DoMessageAction(action, msgID)
log.Error(errors.Wrap(err, "Failed to do action "+action))
}()
})
2020-05-26 06:51:06 +00:00
}
2020-06-20 04:40:34 +00:00
// ServerMessage combines Server and ServerMessage from cchat.
type ServerMessage interface {
cchat.Server
cchat.ServerMessage
}
type state struct {
session cchat.Session
server cchat.Server
actioner cchat.ServerMessageActioner
current func() // stop callback
author string
}
func (s *state) Reset() {
// If we still have the last server to leave, then leave it.
if s.current != nil {
s.current()
}
// Lazy way to reset the state.
*s = state{}
}
func (s *state) hasActions() bool {
2020-06-26 04:11:31 +00:00
return s.actioner != nil
2020-06-20 04:40:34 +00:00
}
// SessionID returns the session ID, or an empty string if there's no session.
func (s *state) SessionID() string {
if s.session != nil {
return s.session.ID()
}
return ""
}
func (s *state) bind(session cchat.Session, server ServerMessage) {
s.session = session
s.server = server
2020-06-26 04:11:31 +00:00
s.actioner, _ = server.(cchat.ServerMessageActioner)
2020-06-20 04:40:34 +00:00
}
func (s *state) setcurrent(fn func()) {
s.current = fn
}