1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2024-12-23 20:56:42 +00:00
cchat-gtk/internal/ui/messages/container/grid.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

297 lines
7 KiB
Go

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"
)
type GridStore struct {
*gtk.Grid
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) Reset() {
c.Grid.GetChildren().Foreach(func(v interface{}) {
// Unsafe assertion ftw.
w := v.(gtk.IWidget).ToWidget()
c.Grid.Remove(w)
w.Destroy()
})
c.messages = map[string]*gridMessage{}
c.messageIDs = []string{}
}
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
}
// 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
}
// 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.
msg.Attach(c.Grid, ix)
// 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
}
// 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
}
// 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-- {
msg := c.messages[c.messageIDs[i]]
// Ignore sending messages.
if msg.presend != nil {
continue
}
// Check.
if msg := msg.GridMessage; isMessage(msg) {
return msg
}
}
return nil
}
// FirstMessage returns the first message.
func (c *GridStore) FirstMessage() GridMessage {
if len(c.messageIDs) > 0 {
return c.messages[c.messageIDs[0]].GridMessage
}
return nil
}
// LastMessage returns the latest message.
func (c *GridStore) LastMessage() GridMessage {
if l := len(c.messageIDs); l > 0 {
return c.messages[c.messageIDs[l-1]].GridMessage
}
return nil
}
// 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
}
func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
// 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) {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
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
}