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

156 lines
4.2 KiB
Go

package message
import (
"context"
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
// Container describes a message container that wraps a state. These methods are
// made for containers to override; methods not meant to be override are not
// exposed and will be done directly on the State.
type Container interface {
// Unwrap unwraps the message container and, if revert is true, revert the
// state to a clean version. Containers must implement this method by
// itself.
Unwrap(revert bool) *State
// UpdateContent updates the underlying content widget.
UpdateContent(content text.Rich, edited bool)
// SetReferenceHighlighter sets the reference highlighter into the message.
SetReferenceHighlighter(refer labeluri.ReferenceHighlighter)
}
// State provides a single generic message container for subpackages
// to use.
type State struct {
gtk.Box
Row *gtk.ListBoxRow // contains Box
class string
ID cchat.ID
Time time.Time
Nonce string
Author *Author
Content *gtk.Box
ContentBody *labeluri.Label
ContentBodyStyle *gtk.StyleContext
MenuItems []menu.Item
}
// NewState creates a new message state with the given ID and nonce. It does not
// update the widgets, so FillContainer should be called afterwards.
func NewState(msg cchat.MessageCreate) *State {
author := msg.Author()
c := NewEmptyState()
c.Author.ID = author.ID()
c.Author.Name.QueueNamer(context.Background(), author)
c.ID = msg.ID()
c.Time = msg.Time()
c.Nonce = msg.Nonce()
return c
}
// NewEmptyState creates a new empty message state. The author should be set
// immediately afterwards; it is invalid once the state is used.
func NewEmptyState() *State {
ctbody := labeluri.NewLabel(text.Rich{})
ctbody.SetVExpand(true)
ctbody.SetHAlign(gtk.ALIGN_START)
ctbody.SetEllipsize(pango.ELLIPSIZE_NONE)
ctbody.SetLineWrap(true)
ctbody.SetLineWrapMode(pango.WRAP_WORD_CHAR)
ctbody.SetXAlign(0) // left align
ctbody.SetSelectable(true)
ctbody.SetTrackVisitedLinks(false)
ctbody.Show()
ctbodyStyle, _ := ctbody.GetStyleContext()
ctbodyStyle.AddClass("message-content")
// Wrap the content label inside a content box.
ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
ctbox.SetHExpand(true)
ctbox.PackStart(ctbody, false, false, 0)
ctbox.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.Show()
row, _ := gtk.ListBoxRowNew()
row.Add(box)
row.Show()
primitives.AddClass(row, "message-row")
gc := &State{
Box: *box,
Row: row,
Author: &Author{},
Content: ctbox,
ContentBody: ctbody,
ContentBodyStyle: ctbodyStyle,
// Time is important, as it is used to sort messages, so we have to be
// careful with this.
Time: time.Now(),
}
// This may either work, or it may cause memory leaks.
row.Connect("destroy", func() { gc.Author.Name.Stop() })
// Bind the custom popup menu to the content label.
gc.ContentBody.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) {
menu.MenuSeparator(m)
menu.MenuItems(m, gc.MenuItems)
})
return gc
}
// SetClass sets the internal row's class.
func (m *State) SetClass(class string) {
if m.class != "" {
primitives.RemoveClass(m.Row, m.class)
}
primitives.AddClass(m.Row, class)
m.class = class
}
// SetReferenceHighlighter sets the reference highlighter into the message.
func (m *State) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
m.ContentBody.SetReferenceHighlighter(r)
}
// UpdateContent replaces the internal content and the widget.
func (m *State) UpdateContent(content text.Rich, edited bool) {
m.ContentBody.SetLabel(content)
if edited {
m.ContentBody.SetRenderer(func(content text.Rich) markup.RenderOutput {
output := markup.RenderCmplx(content)
output.Markup += rich.Small(text.Plain("(edited)")).Markup
return output
})
}
}
func (m *State) Focusable() gtk.IWidget {
return m.Content
}