1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2024-11-16 19:22:51 +00:00
cchat-gtk/internal/ui/messages/container/list.go
2021-05-04 20:30:50 -07:00

486 lines
11 KiB
Go

package container
import (
"log"
"strings"
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
)
type messageKey struct {
id string
nonce bool
}
func nonceKey(nonce string) messageKey { return messageKey{nonce, true} }
func idKey(id cchat.ID) messageKey { return messageKey{id, false} }
// newKey creates a new message key.
func newKey(state *message.State) messageKey {
if state.ID != "" {
return messageKey{state.ID, false}
}
if state.Nonce != "" {
return messageKey{state.Nonce, true}
}
log.Printf("state is missing both ID and Nonce: \n%#v\n", state)
return messageKey{}
}
func parseKeyFromNamer(n primitives.Namer) messageKey {
name, err := n.GetName()
if err != nil {
panic("BUG: failed to get primitive name: " + err.Error())
}
parts := strings.SplitN(name, ":", 2)
if len(parts) != 2 {
return messageKey{id: name}
}
switch parts[0] {
case "id":
return messageKey{id: parts[1]}
case "nonce":
return messageKey{id: parts[1], nonce: true}
default:
panic("unknown prefix in message row name " + parts[0])
}
}
func (key messageKey) expand() (id, nonce string) {
if key.nonce {
return "", key.id
}
return key.id, ""
}
func (key messageKey) name() string {
if key.nonce {
return "nonce:" + key.id
}
return "id:" + key.id
}
// String satisfies the fmt.Stringer interface.
func (key messageKey) String() string { return key.name() }
var messageListCSS = primitives.PrepareClassCSS("message-list", `
.message-list { background: transparent; }
`)
var fallbackAuthor = message.NewCustomAuthor("", text.Plain("self"))
type ListStore struct {
ListBox *gtk.ListBox
Controller Controller
self *message.Author
resetMe bool
messages map[messageKey]*messageRow
}
func NewListStore(ctrl Controller) *ListStore {
listBox, _ := gtk.ListBoxNew()
listBox.SetSelectionMode(gtk.SELECTION_SINGLE)
listBox.Show()
messageListCSS(listBox)
listStore := ListStore{
ListBox: listBox,
Controller: ctrl,
messages: make(map[messageKey]*messageRow, BacklogLimit+1),
self: &fallbackAuthor,
}
var selected bool
listBox.Connect("row-selected", func(listBox *gtk.ListBox, r *gtk.ListBoxRow) {
if r == nil || selected {
if selected {
listBox.UnselectAll()
selected = false
}
ctrl.UnselectMessage()
return
}
id := parseKeyFromNamer(r)
msg := listStore.Message(id.expand())
if msg == nil {
return
}
selected = true
ctrl.SelectMessage(&listStore, msg)
})
return &listStore
}
// Reset resets the list store.
func (c *ListStore) Reset() {
for _, msg := range c.messages {
destroyMsg(msg)
}
// Delegate removing children to the constructor.
c.messages = make(map[messageKey]*messageRow, BacklogLimit+1)
c.self.Name.Stop()
}
// SetSelf sets the current author to presend. If ID is empty or Namer is nil,
// then the fallback author is used instead. The given author will be stopped
// on reset.
func (c *ListStore) SetSelf(self *message.Author) {
if self != nil {
c.self = self
} else {
c.self = &fallbackAuthor
}
}
func (c *ListStore) MessagesLen() int {
return len(c.messages)
}
// 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 *ListStore) SwapMessage(msg MessageRow) bool {
// Unwrap msg from a *messageRow if it's not already.
if mrow, ok := msg.(*messageRow); ok {
msg = mrow.MessageRow
}
state := msg.Unwrap()
// Get the current message's index.
oldMsg, ix := c.findIndex(state.ID)
if ix == -1 {
return false
}
// Remove the previous message off the message map using the key from its
// state.
delete(c.messages, newKey(oldMsg.state))
// Remove the to-be-replaced message box.
// TODO: We should probably reuse the row.
c.ListBox.Remove(oldMsg.state.Row)
// Add a row at index. The actual row we want to delete will be shifted
// downwards.
c.ListBox.Insert(state.Row, ix)
row := messageRow{
MessageRow: msg,
state: state,
}
// Set the message into the map.
c.messages[newKey(state)] = &row
c.bindMessage(&row)
return true
}
// Around returns the message before and after the given ID, or nil if none.
func (c *ListStore) Around(id cchat.ID) (before, after MessageRow) {
gridBefore, gridAfter := c.around(id)
if gridBefore != nil {
before = gridBefore.MessageRow
}
if gridAfter != nil {
after = gridAfter.MessageRow
}
return
}
func (c *ListStore) around(aroundID cchat.ID) (before, after *messageRow) {
var last *messageRow
var next bool
primitives.ForeachChildBackwards(c.ListBox, func(v interface{}) (stop bool) {
id := parseKeyFromNamer(v.(primitives.Namer))
if next {
after = c.message(id.expand())
return true
}
if !id.nonce && id.id == aroundID {
before = last
next = true
return false
}
last = c.message(id.expand())
return false
})
return
}
// findIndex searches backwards for id.
func (c *ListStore) findIndex(findID cchat.ID) (found *messageRow, index int) {
// Faster implementation of findMessage: no map lookup is done until an ID
// match, so the worst case is a single string hash.
index = c.MessagesLen() - 1
primitives.ForeachChildBackwards(c.ListBox, func(v interface{}) (stop bool) {
id := parseKeyFromNamer(v.(primitives.Namer))
if !id.nonce && id.id == findID {
found = c.message(id.expand())
return true
}
index--
return index <= 0
})
// Preserve old behavior.
if found == nil {
index = -1
}
return
}
// Fast path interface.
type messageFinder interface {
findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int)
}
var _ messageFinder = (*ListStore)(nil)
// findMessage finds a message with the given callback as the filter. If presend
// is false, then presend messages are ignored.
func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int) {
var r *messageRow
var i = c.MessagesLen() - 1
primitives.ForeachChildBackwards(c.ListBox, func(v interface{}) (stop bool) {
id := parseKeyFromNamer(v.(primitives.Namer))
gridMsg := c.message(id.expand())
// If gridMsg is actually nil, then we have bigger issues.
if gridMsg != nil {
// Ignore sending messages if presend is false.
if (presend || gridMsg.presend == nil) && fn(gridMsg) {
r = gridMsg
return true
}
}
i--
return i <= 0
})
// Preserve old behavior.
if r == nil {
i = -1
}
return r, i
}
// FindMessage iterates backwards and returns the message if isMessage() returns
// true on that message.
func (c *ListStore) FindMessage(isMessage func(MessageRow) bool) (MessageRow, int) {
msg, ix := c.findMessage(true, func(row *messageRow) bool {
return isMessage(row.MessageRow)
})
return unwrapRow(msg), ix
}
func (c *ListStore) nthMessage(n int) *messageRow {
v := primitives.NthChild(c.ListBox, n)
if v == nil {
return nil
}
id := parseKeyFromNamer(v.(primitives.Namer))
return c.message(id.expand())
}
// NthMessage returns the nth message.
func (c *ListStore) NthMessage(n int) MessageRow {
return unwrapRow(c.nthMessage(n))
}
// Message finds the message state in the container. It is not thread-safe. This
// exists for backwards compatibility.
func (c *ListStore) Message(msgID cchat.ID, nonce string) MessageRow {
return unwrapRow(c.message(msgID, nonce))
}
func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
// Search using the ID first.
m, ok := c.messages[idKey(msgID)]
if ok {
return m
}
// Is this an existing message?
if nonce != "" {
// Things in this map are guaranteed to have presend != nil.
m, ok := c.messages[nonceKey(nonce)]
// This is honestly pretty dumb, but whatever.
// TODO: make message() getter not set.
if ok {
// Replace the nonce key with ID.
delete(c.messages, nonceKey(nonce))
c.messages[idKey(msgID)] = m
c.bindMessage(m)
// Set the right ID.
m.presend.SetDone(msgID)
// Destroy the presend struct.
m.presend = nil
return m
}
}
return nil
}
func (c *ListStore) bindMessage(msgc *messageRow) {
// Bind the message ID to the row so we can easily do a lookup.
key := messageKey{
id: msgc.state.ID,
}
if msgc.state.Nonce != "" {
key.id = msgc.state.Nonce
key.nonce = true
}
msgc.state.Row.SetName(key.name())
msgc.MessageRow.SetReferenceHighlighter(c)
c.Controller.BindMenu(msgc.MessageRow)
}
func (c *ListStore) AddMessageAt(msg MessageRow, ix int) {
state := msg.Unwrap()
defer c.Controller.AuthorEvent(state.Author.ID)
// Do not attempt to update before insertion (aka upsert).
if msgc := c.message(state.ID, state.Nonce); msgc != nil {
// This is kind of expensive, but it shouldn't really matter.
c.SwapMessage(msg)
return
}
// Attempt to guess if this is a presend message or not. This should be
// unwrapped once it's finalized.
presend, _ := msg.(message.Presender)
msgc := &messageRow{
MessageRow: msg,
presend: presend,
state: state,
}
// Add the message. If before is nil, then the to-be-inserted message is the
// earliest message, therefore we prepend it.
if ix < 0 {
ix = 0
c.ListBox.Prepend(state.Row)
} else {
ix++ // insert right after
// Fast path: Insert did appear a lot on profiles, so we can try and use
// Add over Insert when we know.
if c.MessagesLen() == ix {
c.ListBox.Add(state.Row)
} else {
c.ListBox.Insert(state.Row, ix)
}
}
c.messages[newKey(state)] = msgc
c.bindMessage(msgc)
}
// PopMessage deletes a message off of the list and return the deleted message.
func (c *ListStore) PopMessage(id cchat.ID) (msg MessageRow) {
// Get the raw element to delete it off the list.
gridMsg, _ := c.findIndex(id)
if gridMsg == nil {
return nil
}
msg = gridMsg.MessageRow
// Remove off of the Gtk grid.
destroyMsg(gridMsg)
// Delete off the map.
delete(c.messages, idKey(id))
return
}
// DeleteEarliest deletes the n earliest messages. It does nothing if n is or
// less than 0.
func (c *ListStore) DeleteEarliest(n int) {
if n <= 0 {
return
}
// Since container/list nils out the next element, we can't just call Next
// after deleting, so we have to call Next manually before Removing.
primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) {
id := parseKeyFromNamer(v.(primitives.Namer))
mr, ok := c.messages[id]
if !ok {
log.Panicln("message with ID", id, "not found in map")
}
delete(c.messages, id)
destroyMsg(mr)
n--
return n == 0
})
}
func (c *ListStore) HighlightReference(ref markup.ReferenceSegment) {
msg := c.message(ref.MessageID(), "")
if msg != nil {
c.Highlight(msg)
}
}
func (c *ListStore) Highlight(msg MessageRow) {
gts.ExecAsync(func() {
state := msg.Unwrap()
state.Row.GrabFocus()
c.ListBox.DragHighlightRow(state.Row)
gts.DoAfter(2*time.Second, c.ListBox.DragUnhighlightRow)
})
}
func destroyMsg(row *messageRow) {
row.Revert()
row.state.Author.Name.Stop()
row.state.Row.Destroy()
}