2020-06-13 07:29:32 +00:00
|
|
|
package container
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"github.com/diamondburned/cchat"
|
|
|
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
|
|
|
"github.com/gotk3/gotk3/gtk"
|
2020-08-20 23:53:13 +00:00
|
|
|
"github.com/pkg/errors"
|
2020-06-13 07:29:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type GridStore struct {
|
2020-07-03 03:22:48 +00:00
|
|
|
*gtk.Grid
|
2020-06-13 07:29:32 +00:00
|
|
|
|
|
|
|
Construct Constructor
|
|
|
|
Controller Controller
|
|
|
|
|
|
|
|
messages map[string]*gridMessage
|
|
|
|
messageIDs []string // ids or nonces
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewGridStore(constr Constructor, ctrl Controller) *GridStore {
|
|
|
|
grid, _ := gtk.GridNew()
|
|
|
|
grid.SetColumnSpacing(ColumnSpacing)
|
|
|
|
grid.SetRowSpacing(5)
|
|
|
|
grid.SetMarginStart(5)
|
|
|
|
grid.SetMarginEnd(5)
|
|
|
|
grid.Show()
|
|
|
|
|
|
|
|
primitives.AddClass(grid, "message-grid")
|
|
|
|
|
|
|
|
return &GridStore{
|
|
|
|
Grid: grid,
|
|
|
|
Construct: constr,
|
|
|
|
Controller: ctrl,
|
|
|
|
messages: map[string]*gridMessage{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *GridStore) MessagesLen() int {
|
|
|
|
return len(c.messages)
|
|
|
|
}
|
|
|
|
|
|
|
|
// findIndex searches backwards for idnonce.
|
|
|
|
func (c *GridStore) findIndex(idnonce string) int {
|
|
|
|
for i := len(c.messageIDs) - 1; i >= 0; i-- {
|
|
|
|
if c.messageIDs[i] == idnonce {
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:53:13 +00:00
|
|
|
type CoordinateTranslator interface {
|
|
|
|
TranslateCoordinates(dest gtk.IWidget, srcX int, srcY int) (destX int, destY int, e error)
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ CoordinateTranslator = (*gtk.Widget)(nil)
|
|
|
|
|
|
|
|
func (c *GridStore) TranslateCoordinates(parent gtk.IWidget, msg GridMessage) (y int) {
|
|
|
|
i := c.findIndex(msg.ID())
|
|
|
|
if i < 0 {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
m, _ := c.messages[c.messageIDs[i]]
|
|
|
|
w, _ := m.Focusable().(CoordinateTranslator)
|
|
|
|
|
|
|
|
// x is not needed.
|
|
|
|
_, y, err := w.TranslateCoordinates(parent, 0, 0)
|
|
|
|
if err != nil {
|
|
|
|
log.Error(errors.Wrap(err, "Failed to translate coords while focusing"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// log.Println("X:", x)
|
|
|
|
// log.Println("Y:", y)
|
|
|
|
|
|
|
|
return y
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// Swap changes the message with the ID to the given message. This provides a
|
|
|
|
// low level API for edits that need a new Attach method.
|
|
|
|
//
|
|
|
|
// TODO: combine compact and full so they share the same attach method.
|
|
|
|
func (c *GridStore) SwapMessage(msg GridMessage) bool {
|
|
|
|
// Get the current message's index.
|
|
|
|
var ix = c.findIndex(msg.ID())
|
|
|
|
if ix == -1 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:53:13 +00:00
|
|
|
// Wrap msg inside a *gridMessage if it's not already.
|
|
|
|
mg, ok := msg.(*gridMessage)
|
|
|
|
if !ok {
|
|
|
|
mg = &gridMessage{GridMessage: msg}
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// Add a row at index. The actual row we want to delete will be shifted
|
|
|
|
// downwards.
|
|
|
|
c.Grid.InsertRow(ix)
|
|
|
|
|
|
|
|
// Let the new message be attached on top of the to-be-replaced message.
|
2020-08-20 23:53:13 +00:00
|
|
|
mg.Attach(c.Grid, ix)
|
|
|
|
|
|
|
|
// Set the message into the map.
|
|
|
|
c.messages[mg.ID()] = mg
|
2020-06-13 07:29:32 +00:00
|
|
|
|
|
|
|
// Delete the to-be-replaced message, which we have shifted downwards
|
|
|
|
// earlier, so we add 1.
|
|
|
|
c.Grid.RemoveRow(ix + 1)
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Before returns the message before the given ID, or nil if none.
|
|
|
|
func (c *GridStore) Before(id string) GridMessage {
|
|
|
|
return c.getOffsetted(id, -1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// After returns the message after the given ID, or nil if none.
|
|
|
|
func (c *GridStore) After(id string) GridMessage {
|
|
|
|
return c.getOffsetted(id, 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *GridStore) getOffsetted(id string, offset int) GridMessage {
|
|
|
|
// Get the current index.
|
|
|
|
var ix = c.findIndex(id)
|
|
|
|
if ix == -1 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
ix += offset
|
|
|
|
|
|
|
|
if ix < 0 || ix >= len(c.messages) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.messages[c.messageIDs[ix]].GridMessage
|
|
|
|
}
|
|
|
|
|
2020-06-17 22:58:38 +00:00
|
|
|
// LatestMessageFrom returns the latest message with the given user ID. This is
|
|
|
|
// used for the input prompt.
|
|
|
|
func (c *GridStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
|
|
|
// FindMessage already looks from the latest messages.
|
|
|
|
var msg = c.FindMessage(func(msg GridMessage) bool {
|
|
|
|
return msg.AuthorID() == userID
|
|
|
|
})
|
|
|
|
|
|
|
|
if msg == nil {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
|
|
|
|
return msg.ID(), true
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// FindMessage iterates backwards and returns the message if isMessage() returns
|
|
|
|
// true on that message.
|
|
|
|
func (c *GridStore) FindMessage(isMessage func(msg GridMessage) bool) GridMessage {
|
|
|
|
for i := len(c.messageIDs) - 1; i >= 0; i-- {
|
2020-06-17 22:58:38 +00:00
|
|
|
msg := c.messages[c.messageIDs[i]]
|
|
|
|
// Ignore sending messages.
|
|
|
|
if msg.presend != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// Check.
|
|
|
|
if msg := msg.GridMessage; isMessage(msg) {
|
2020-06-13 07:29:32 +00:00
|
|
|
return msg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:53:13 +00:00
|
|
|
// NthMessage returns the nth message.
|
|
|
|
func (c *GridStore) NthMessage(n int) GridMessage {
|
|
|
|
if len(c.messageIDs) > 0 && n >= 0 && n < len(c.messageIDs) {
|
|
|
|
return c.messages[c.messageIDs[n]].GridMessage
|
2020-06-13 21:51:03 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:53:13 +00:00
|
|
|
// FirstMessage returns the first message.
|
|
|
|
func (c *GridStore) FirstMessage() GridMessage {
|
|
|
|
return c.NthMessage(0)
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// LastMessage returns the latest message.
|
|
|
|
func (c *GridStore) LastMessage() GridMessage {
|
2020-08-20 23:53:13 +00:00
|
|
|
return c.NthMessage(c.MessagesLen() - 1)
|
2020-06-13 07:29:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Message finds the message state in the container. It is not thread-safe. This
|
|
|
|
// exists for backwards compatibility.
|
|
|
|
func (c *GridStore) Message(msg cchat.MessageHeader) GridMessage {
|
|
|
|
if m := c.message(msg); m != nil {
|
|
|
|
return m.GridMessage
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *GridStore) message(msg cchat.MessageHeader) *gridMessage {
|
|
|
|
// Search using the ID first.
|
|
|
|
m, ok := c.messages[msg.ID()]
|
|
|
|
if ok {
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
|
|
|
// Is this an existing message?
|
|
|
|
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
|
|
|
var nonce = noncer.Nonce()
|
|
|
|
|
|
|
|
// Things in this map are guaranteed to have presend != nil.
|
|
|
|
m, ok := c.messages[nonce]
|
|
|
|
if ok {
|
|
|
|
// Replace the nonce key with ID.
|
|
|
|
delete(c.messages, nonce)
|
|
|
|
c.messages[msg.ID()] = m
|
|
|
|
|
|
|
|
// Set the right ID.
|
|
|
|
m.presend.SetDone(msg.ID())
|
|
|
|
// Destroy the presend struct.
|
|
|
|
m.presend = nil
|
|
|
|
|
|
|
|
// Replace the nonce inside the ID slice with the actual ID.
|
|
|
|
if ix := c.findIndex(nonce); ix > -1 {
|
|
|
|
c.messageIDs[ix] = msg.ID()
|
|
|
|
} else {
|
|
|
|
log.Error(fmt.Errorf("Missed ID %s in slice index %d", msg.ID(), ix))
|
|
|
|
}
|
|
|
|
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddPresendMessage inserts an input.PresendMessage into the container and
|
|
|
|
// returning a wrapped widget interface.
|
|
|
|
func (c *GridStore) AddPresendMessage(msg input.PresendMessage) PresendGridMessage {
|
|
|
|
presend := c.Construct.NewPresendMessage(msg)
|
|
|
|
|
|
|
|
msgc := &gridMessage{
|
|
|
|
GridMessage: presend,
|
|
|
|
presend: presend,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the message into the grid.
|
|
|
|
msgc.Attach(c.Grid, c.MessagesLen())
|
|
|
|
// Append the NONCE.
|
|
|
|
c.messageIDs = append(c.messageIDs, msgc.Nonce())
|
|
|
|
// Set the NONCE into the message map.
|
|
|
|
c.messages[msgc.Nonce()] = msgc
|
|
|
|
|
|
|
|
return presend
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:53:13 +00:00
|
|
|
func (c *GridStore) PrependMessageUnsafe(msg cchat.MessageCreate) {
|
|
|
|
msgc := &gridMessage{
|
|
|
|
GridMessage: c.Construct.NewMessage(msg),
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Grid.InsertRow(0)
|
|
|
|
msgc.Attach(c.Grid, 0)
|
|
|
|
|
|
|
|
// Prepend the message ID.
|
|
|
|
c.messageIDs = append(c.messageIDs, "")
|
|
|
|
copy(c.messageIDs[1:], c.messageIDs)
|
|
|
|
c.messageIDs[0] = msgc.ID()
|
|
|
|
|
|
|
|
// Set the message into the map.
|
|
|
|
c.messages[msgc.ID()] = msgc
|
|
|
|
|
|
|
|
c.Controller.BindMenu(msgc)
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
2020-07-04 04:41:12 +00:00
|
|
|
// Call the event handler last.
|
|
|
|
defer c.Controller.AuthorEvent(msg.Author())
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// Attempt to update before insertion (aka upsert).
|
|
|
|
if msgc := c.Message(msg); msgc != nil {
|
|
|
|
msgc.UpdateAuthor(msg.Author())
|
|
|
|
msgc.UpdateContent(msg.Content(), false)
|
|
|
|
msgc.UpdateTimestamp(msg.Time())
|
|
|
|
|
|
|
|
c.Controller.BindMenu(msgc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
msgc := &gridMessage{
|
|
|
|
GridMessage: c.Construct.NewMessage(msg),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy from PresendMessage.
|
|
|
|
msgc.Attach(c.Grid, c.MessagesLen())
|
|
|
|
c.messageIDs = append(c.messageIDs, msgc.ID())
|
|
|
|
c.messages[msgc.ID()] = msgc
|
|
|
|
|
|
|
|
c.Controller.BindMenu(msgc)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *GridStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
|
2020-07-04 04:41:12 +00:00
|
|
|
// Call the event handler last.
|
|
|
|
defer c.Controller.AuthorEvent(msg.Author())
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
if msgc := c.Message(msg); msgc != nil {
|
|
|
|
if author := msg.Author(); author != nil {
|
|
|
|
msgc.UpdateAuthor(author)
|
|
|
|
}
|
|
|
|
if content := msg.Content(); !content.Empty() {
|
|
|
|
msgc.UpdateContent(content, true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *GridStore) DeleteMessageUnsafe(msg cchat.MessageDelete) {
|
|
|
|
c.PopMessage(msg.ID())
|
|
|
|
}
|
|
|
|
|
|
|
|
// PopMessage deletes a message off of the list and return the deleted message.
|
|
|
|
func (c *GridStore) PopMessage(id string) (msg GridMessage) {
|
|
|
|
// Search for the index.
|
|
|
|
var ix = c.findIndex(id)
|
|
|
|
if ix < 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Grab the message before deleting.
|
|
|
|
msg = c.messages[id]
|
|
|
|
|
|
|
|
// Remove off of the Gtk grid.
|
|
|
|
c.Grid.RemoveRow(ix)
|
|
|
|
// Pop off the slice.
|
|
|
|
c.messageIDs = append(c.messageIDs[:ix], c.messageIDs[ix+1:]...)
|
|
|
|
// Delete off the map.
|
|
|
|
delete(c.messages, id)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|