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

213 lines
5.8 KiB
Go

package container
import (
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk"
)
// BacklogLimit is the maximum number of messages to store in the container at
// once.
const BacklogLimit = 50
type MessageRow interface {
message.Container
GrabFocus()
}
type PresendMessageRow interface {
MessageRow
message.Presender
}
// Container is a generic messages container for children messages for children
// packages.
type Container interface {
gtk.IWidget
cchat.MessagesContainer
// Reset resets the message container to its original state.
Reset()
// SetSelf sets the author for the current user.
SetSelf(self *message.Author)
// NewPresendMessage creates and adds a presend message state into the list.
NewPresendMessage(state *message.PresendState) PresendMessageRow
// AddMessageAt adds a new message into the list at the given index.
AddMessageAt(row MessageRow, ix int)
// MessagesLen returns the current number of messages.
MessagesLen() int
// NthMessage returns the nth message in the buffer or nil if there's
// nothing.
NthMessage(ix int) MessageRow
// Message finds and returns the message, if any. It performs maximum 2
// constant-time lookups.
Message(id cchat.ID, nonce string) MessageRow
// FindMessage finds a message that satisfies the given callback. It
// iterates the message buffer from latest to earliest.
FindMessage(isMessage func(MessageRow) bool) (MessageRow, int)
// Highlight temporarily highlights the given message for a short while.
Highlight(msg MessageRow)
// UI methods.
SetFocusHAdjustment(*gtk.Adjustment)
SetFocusVAdjustment(*gtk.Adjustment)
}
// UpdateMessage is a convenient function to update a message in the container.
// It does nothing if the message is not found.
func UpdateMessage(ct Container, update cchat.MessageUpdate) {
if msg := ct.Message(update.ID(), ""); msg != nil {
msg.UpdateContent(update.Content(), true)
}
}
// LatestMessageFrom returns the latest message from the given author ID.
func LatestMessageFrom(ct Container, authorID cchat.ID) (MessageRow, int) {
finder, ok := ct.(messageFinder)
if !ok {
return ct.FindMessage(func(msg MessageRow) bool {
return msg.Unwrap().Author.ID == authorID
})
}
msg, ix := finder.findMessage(true, func(msg *messageRow) bool {
return msg.state.Author.ID == authorID
})
return unwrapRow(msg), ix
}
// FirstMessage returns the first message in the buffer. Nil is returned if
// there's nothing.
func FirstMessage(ct Container) MessageRow {
return ct.NthMessage(0)
}
// LastMessage returns the last message in the buffer or nil if there's nothing.
func LastMessage(ct Container) MessageRow {
return ct.NthMessage(ct.MessagesLen() - 1)
}
// InsertPosition returns the message that is before the given time (or nil) and
// the new index of the message with the given timestamp. If -1 is returned,
// then there is no message prior, and the message should be prepended on top.
func InsertPosition(ct Container, t time.Time) (MessageRow, int) {
var row MessageRow
var mIx int
finder, ok := ct.(messageFinder)
if !ok {
row, mIx = ct.FindMessage(func(msg MessageRow) bool {
return t.After(msg.Unwrap().Time)
})
} else {
// Iterate and compare timestamp to find where to insert a message. Note
// that "before" is the message that will go before the to-be-inserted
// method.
msg, ix := finder.findMessage(true, func(msg *messageRow) bool {
return t.After(msg.state.Time)
})
row = unwrapRow(msg)
mIx = ix
}
return row, mIx
}
// Controller is for menu actions.
type Controller interface {
// Connector is used for button press events to unselect messages.
primitives.Connector
// BindMenu expects the controller to add actioner into the message.
BindMenu(MessageRow)
// Bottomed returns whether or not the message scroller is at the bottom.
Bottomed() bool
// AuthorEvent is called on message create/update. This is used to update
// the typer state.
AuthorEvent(authorID cchat.ID)
// SelectMessage is called when a message is selected.
SelectMessage(list *ListStore, msg MessageRow)
// UnselectMessage is called when the message selection is cleared.
UnselectMessage()
}
const ColumnSpacing = 8
// ListContainer is an implementation of Container, which allows flexible
// message grids.
type ListContainer struct {
*handy.Clamp
*ListStore
Controller
}
// messageRow w/ required internals
type messageRow struct {
MessageRow
state *message.State
presend message.Presender // this shouldn't be here but i'm lazy
}
// unwrapRow is a helper that unwraps a messageRow if it's not nil. If it's nil,
// then a nil interface is returned.
func unwrapRow(msg *messageRow) MessageRow {
if msg == nil || msg.MessageRow == nil {
return nil
}
return msg.MessageRow
}
func NewListContainer(ctrl Controller) *ListContainer {
listStore := NewListStore(ctrl)
listStore.ListBox.Show()
clamp := handy.ClampNew()
clamp.SetMaximumSize(800)
clamp.SetTighteningThreshold(600)
clamp.SetHExpand(true)
clamp.SetVExpand(true)
clamp.Add(listStore.ListBox)
clamp.Show()
return &ListContainer{
Clamp: clamp,
ListStore: listStore,
Controller: ctrl,
}
}
// CleanMessages cleans up the oldest messages if the user is scrolled to the
// bottom. True is returned if there were changes.
func (c *ListContainer) CleanMessages() bool {
// Determine if the user is scrolled to the bottom for cleaning up.
if c.Bottomed() {
// Clean up the backlog.
if delta := c.MessagesLen() - BacklogLimit; delta > 0 {
c.DeleteEarliest(delta)
return true
}
}
return false
}
func (c *ListContainer) SetFocusHAdjustment(adj *gtk.Adjustment) {
c.ListBox.SetFocusHAdjustment(adj)
}
func (c *ListContainer) SetFocusVAdjustment(adj *gtk.Adjustment) {
c.ListBox.SetFocusVAdjustment(adj)
}