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

251 lines
7.6 KiB
Go

package cozy
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
)
// Unwrapper provides an interface for messages to be unwrapped. This is used to
// convert between collapsed and full messages.
type Unwrapper interface {
Unwrap() *message.GenericContainer
}
var (
_ Unwrapper = (*CollapsedMessage)(nil)
_ Unwrapper = (*CollapsedSendingMessage)(nil)
_ Unwrapper = (*FullMessage)(nil)
_ Unwrapper = (*FullSendingMessage)(nil)
)
// Collapsible is an interface for cozy messages to return whether or not
// they're full or collapsed.
type Collapsible interface {
// Compact returns true if the message is a compact one and not full.
Collapsed() bool
}
var (
_ Collapsible = (*CollapsedMessage)(nil)
_ Collapsible = (*CollapsedSendingMessage)(nil)
_ Collapsible = (*FullMessage)(nil)
_ Collapsible = (*FullSendingMessage)(nil)
)
const (
AvatarSize = 40
AvatarMargin = 10
)
type Container struct {
*container.ListContainer
}
func NewContainer(ctrl container.Controller) *Container {
c := &Container{}
c.ListContainer = container.NewListContainer(c, ctrl)
primitives.AddClass(c, "cozy-container")
return c
}
func (c *Container) NewMessage(msg cchat.MessageCreate) container.MessageRow {
// We're not checking for a collapsed message here anymore, as the
// CreateMessage method will do that.
// // Is the latest message of the same author? If yes, display it as a
// // collapsed message.
// if c.lastMessageIsAuthor(msg.Author().ID()) {
// return NewCollapsedMessage(msg)
// }
full := NewFullMessage(msg)
author := msg.Author()
// Try and reuse an existing avatar if the author has one.
if avatarURL := author.Avatar(); avatarURL != "" {
// Try reusing the avatar, but fetch it from the interndet if we can't
// reuse. The reuse function does this for us.
c.reuseAvatar(author.ID(), author.Avatar(), full)
}
return full
}
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendMessageRow {
// We can do the check here since we're never using NewPresendMessage for
// backlog messages.
if c.lastMessageIsAuthor(msg.AuthorID(), msg.Author().String(), 0) {
return NewCollapsedSendingMessage(msg)
}
full := NewFullSendingMessage(msg)
// Try and see if we can reuse the avatar, and fallback if possible. The
// avatar URL passed in here will always yield an equal.
c.reuseAvatar(msg.AuthorID(), msg.AuthorAvatarURL(), &full.FullMessage)
return full
}
func (c *Container) findAuthorID(authorID string) container.MessageRow {
// Search the old author if we have any.
return c.ListStore.FindMessage(func(msgc container.MessageRow) bool {
return msgc.AuthorID() == authorID
})
}
// reuseAvatar tries to search past messages with the same author ID and URL for
// the image. It will fetch anew if there's none.
func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) {
// Is this a message that we can work with? We have to assert to
// FullSendingMessage because that's where our messages are.
var lastAuthorMsg = c.findAuthorID(authorID)
// Borrow the avatar pixbuf, but only if the avatar URL is the same.
p, ok := lastAuthorMsg.(AvatarPixbufCopier)
if ok && lastAuthorMsg.AvatarURL() == avatarURL {
p.CopyAvatarPixbuf(full.Avatar.Image)
full.Avatar.ManuallySetURL(avatarURL)
} else {
// We can't borrow, so we need to fetch it anew.
full.Avatar.SetURL(avatarURL)
}
}
func (c *Container) lastMessageIsAuthor(id cchat.ID, name string, offset int) bool {
// Get the offfsetth message from last.
var last = c.ListStore.NthMessage((c.ListStore.MessagesLen() - 1) + offset)
return gridMessageIsAuthor(last, id, name)
}
func gridMessageIsAuthor(gridMsg container.MessageRow, id cchat.ID, name string) bool {
return gridMsg != nil &&
gridMsg.AuthorID() == id &&
gridMsg.AuthorName() == name
}
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
// Create the message in the parent's handler. This handler will also
// wipe old messages.
c.ListContainer.CreateMessageUnsafe(msg)
// Did the handler wipe old messages? It will only do so if the user is
// scrolled to the bottom.
if c.ListContainer.CleanMessages() {
// We need to uncollapse the first (top) message. No length check is
// needed here, as we just inserted a message.
c.uncompact(c.FirstMessage())
}
switch msg.ID() {
// Should we collapse this message? Yes, if the current message is
// inserted at the end and its author is the same as the last author.
case c.ListContainer.LastMessage().ID():
author := msg.Author()
if c.lastMessageIsAuthor(author.ID(), author.Name().String(), -1) {
c.compact(c.ListContainer.LastMessage())
}
// If we've prepended the message, then see if we need to collapse the
// second message.
case c.ListContainer.FirstMessage().ID():
if sec := c.NthMessage(1); sec != nil {
// The author is the same; collapse.
author := msg.Author()
if gridMessageIsAuthor(sec, author.ID(), author.Name().String()) {
c.compact(sec)
}
}
}
})
}
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
gts.ExecAsync(func() {
c.UpdateMessageUnsafe(msg)
})
}
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
gts.ExecAsync(func() {
// Get the previous and next message before deleting. We'll need them to
// evaluate whether we need to change anything.
prev, next := c.ListStore.Around(msg.ID())
// The function doesn't actually try and re-collapse the bottom message
// when a sandwiched message is deleted. This is fine.
// Delete the message off of the parent's container.
msg := c.ListStore.PopMessage(msg.ID())
// Don't calculate if we don't have any messages, or no messages before
// and after.
if c.ListStore.MessagesLen() == 0 || prev == nil || next == nil {
return
}
// Check if the last message is the author's (relative to i):
if prev.AuthorID() == msg.AuthorID() {
// If the author is the same, then we don't need to uncollapse the
// message.
return
}
// If the next message (relative to i) is not the deleted message's
// author, then we don't need to uncollapse it.
if next.AuthorID() != msg.AuthorID() {
return
}
// Uncompact or turn the message to a full one.
c.uncompact(next)
})
}
func (c *Container) uncompact(msg container.MessageRow) {
// We should only uncompact the message if it's compacted in the first
// place.
if collapse, ok := msg.(Collapsible); !ok || !collapse.Collapsed() {
return
}
// We can't unwrap if the message doesn't implement Unwrapper.
uw, ok := msg.(Unwrapper)
if !ok {
return
}
// Start the "lengthy" uncollapse process.
full := WrapFullMessage(uw.Unwrap())
// Update the container to reformat everything including the timestamps.
message.RefreshContainer(full, full.GenericContainer)
// Update the avatar if needed be, since we're now showing it.
c.reuseAvatar(msg.AuthorID(), msg.AvatarURL(), full)
// Swap the old next message out for a new one.
c.ListStore.SwapMessage(full)
}
func (c *Container) compact(msg container.MessageRow) {
// Exit if the message is already collapsed.
if collapse, ok := msg.(Collapsible); !ok || collapse.Collapsed() {
return
}
uw, ok := msg.(Unwrapper)
if !ok {
return
}
compact := WrapCollapsedMessage(uw.Unwrap())
message.RefreshContainer(compact, compact.GenericContainer)
c.ListStore.SwapMessage(compact)
}