2020-06-06 07:44:36 +00:00
|
|
|
package cozy
|
|
|
|
|
|
|
|
import (
|
2020-06-07 04:27:28 +00:00
|
|
|
"github.com/diamondburned/cchat"
|
2020-06-13 07:29:32 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
2020-06-07 04:27:28 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
|
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
2020-06-13 07:29:32 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
2020-06-07 04:27:28 +00:00
|
|
|
)
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// Unwrapper provides an interface for messages to be unwrapped. This is used to
|
|
|
|
// convert between collapsed and full messages.
|
|
|
|
type Unwrapper interface {
|
2021-01-02 09:24:14 +00:00
|
|
|
Unwrap() *message.GenericContainer
|
2020-06-07 04:27:28 +00:00
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
var (
|
|
|
|
_ Unwrapper = (*CollapsedMessage)(nil)
|
|
|
|
_ Unwrapper = (*CollapsedSendingMessage)(nil)
|
|
|
|
_ Unwrapper = (*FullMessage)(nil)
|
|
|
|
_ Unwrapper = (*FullSendingMessage)(nil)
|
|
|
|
)
|
|
|
|
|
2020-06-13 21:51:03 +00:00
|
|
|
// 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)
|
|
|
|
)
|
|
|
|
|
2020-06-07 04:27:28 +00:00
|
|
|
const (
|
|
|
|
AvatarSize = 40
|
|
|
|
AvatarMargin = 10
|
2020-06-06 07:44:36 +00:00
|
|
|
)
|
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
var messageConstructors = container.Constructor{
|
|
|
|
NewMessage: NewMessage,
|
|
|
|
NewPresendMessage: NewPresendMessage,
|
2020-06-07 04:27:28 +00:00
|
|
|
}
|
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
func NewMessage(
|
|
|
|
msg cchat.MessageCreate, before container.MessageRow) container.MessageRow {
|
2020-06-07 04:27:28 +00:00
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
if gridMessageIsAuthor(before, msg.Author()) {
|
|
|
|
return NewCollapsedMessage(msg)
|
2020-06-07 04:27:28 +00:00
|
|
|
}
|
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
return NewFullMessage(msg)
|
2020-06-07 04:27:28 +00:00
|
|
|
}
|
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
func NewPresendMessage(
|
|
|
|
msg input.PresendMessage, before container.MessageRow) container.PresendMessageRow {
|
|
|
|
|
|
|
|
if gridMessageIsAuthor(before, msg.Author()) {
|
2020-06-13 07:29:32 +00:00
|
|
|
return NewCollapsedSendingMessage(msg)
|
2020-06-07 07:06:13 +00:00
|
|
|
}
|
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
return NewFullSendingMessage(msg)
|
|
|
|
}
|
2020-06-13 07:29:32 +00:00
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
type Container struct {
|
|
|
|
*container.ListContainer
|
|
|
|
}
|
2020-06-13 07:29:32 +00:00
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
func NewContainer(ctrl container.Controller) *Container {
|
|
|
|
c := container.NewListContainer(ctrl, messageConstructors)
|
|
|
|
primitives.AddClass(c, "cozy-container")
|
|
|
|
return &Container{ListContainer: c}
|
2020-06-07 04:27:28 +00:00
|
|
|
}
|
|
|
|
|
2021-01-02 09:24:14 +00:00
|
|
|
func (c *Container) findAuthorID(authorID string) container.MessageRow {
|
2020-06-07 04:27:28 +00:00
|
|
|
// Search the old author if we have any.
|
2021-01-02 09:24:14 +00:00
|
|
|
return c.ListStore.FindMessage(func(msgc container.MessageRow) bool {
|
2021-01-05 02:05:33 +00:00
|
|
|
return msgc.Author().ID() == authorID
|
2020-06-07 04:27:28 +00:00
|
|
|
})
|
2020-06-13 07:29:32 +00:00
|
|
|
}
|
2020-06-07 04:27:28 +00:00
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// 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) {
|
2020-06-07 04:27:28 +00:00
|
|
|
// Is this a message that we can work with? We have to assert to
|
|
|
|
// FullSendingMessage because that's where our messages are.
|
2020-06-13 07:29:32 +00:00
|
|
|
var lastAuthorMsg = c.findAuthorID(authorID)
|
|
|
|
|
|
|
|
// Borrow the avatar pixbuf, but only if the avatar URL is the same.
|
|
|
|
p, ok := lastAuthorMsg.(AvatarPixbufCopier)
|
2021-01-05 02:05:33 +00:00
|
|
|
if ok && lastAuthorMsg.Author().Avatar() == avatarURL {
|
|
|
|
if p.CopyAvatarPixbuf(full.Avatar.Image) {
|
|
|
|
full.Avatar.ManuallySetURL(avatarURL)
|
|
|
|
return
|
|
|
|
}
|
2020-06-07 04:27:28 +00:00
|
|
|
}
|
2020-06-13 07:29:32 +00:00
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
// We can't borrow, so we need to fetch it anew.
|
|
|
|
full.Avatar.SetURL(avatarURL)
|
2020-08-20 23:53:13 +00:00
|
|
|
}
|
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
// lastMessageIsAuthor removed - assuming index before insertion is harmful.
|
|
|
|
|
|
|
|
func gridMessageIsAuthor(gridMsg container.MessageRow, author cchat.Author) bool {
|
|
|
|
if gridMsg == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
leftAuthor := gridMsg.Author()
|
|
|
|
return true &&
|
|
|
|
leftAuthor.ID() == author.ID() &&
|
|
|
|
leftAuthor.Name().String() == author.Name().String()
|
2020-12-31 03:00:00 +00:00
|
|
|
}
|
2020-11-06 04:01:00 +00:00
|
|
|
|
2020-06-13 21:51:03 +00:00
|
|
|
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
|
|
|
gts.ExecAsync(func() {
|
2020-12-31 03:00:00 +00:00
|
|
|
// Create the message in the parent's handler. This handler will also
|
|
|
|
// wipe old messages.
|
2021-01-05 02:05:33 +00:00
|
|
|
row := c.ListContainer.CreateMessageUnsafe(msg)
|
|
|
|
|
|
|
|
// Is this a full message? If so, then we should fetch the avatar when
|
|
|
|
// we can.
|
|
|
|
if full, ok := row.(*FullMessage); ok {
|
|
|
|
author := msg.Author()
|
|
|
|
avatarURL := author.Avatar()
|
|
|
|
|
|
|
|
// Try and reuse an existing avatar if the author has one.
|
|
|
|
if avatarURL != "" {
|
|
|
|
// Try reusing the avatar, but fetch it from the internet if we can't
|
|
|
|
// reuse. The reuse function does this for us.
|
|
|
|
c.reuseAvatar(author.ID(), avatarURL, full)
|
|
|
|
}
|
|
|
|
}
|
2020-12-31 03:00:00 +00:00
|
|
|
|
|
|
|
// Did the handler wipe old messages? It will only do so if the user is
|
|
|
|
// scrolled to the bottom.
|
2021-01-02 09:24:14 +00:00
|
|
|
if c.ListContainer.CleanMessages() {
|
2020-12-31 03:00:00 +00:00
|
|
|
// We need to uncollapse the first (top) message. No length check is
|
|
|
|
// needed here, as we just inserted a message.
|
|
|
|
c.uncompact(c.FirstMessage())
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we've prepended the message, then see if we need to collapse the
|
|
|
|
// second message.
|
2021-01-05 02:05:33 +00:00
|
|
|
if first := c.ListContainer.FirstMessage(); first != nil && first.ID() == msg.ID() {
|
|
|
|
// If the author is the same, then collapse.
|
|
|
|
if sec := c.NthMessage(1); sec != nil && gridMessageIsAuthor(sec, msg.Author()) {
|
|
|
|
c.compact(sec)
|
2020-11-05 19:08:30 +00:00
|
|
|
}
|
2020-12-31 03:00:00 +00:00
|
|
|
}
|
2020-11-05 19:08:30 +00:00
|
|
|
})
|
|
|
|
}
|
2020-06-13 21:51:03 +00:00
|
|
|
|
2020-11-05 19:08:30 +00:00
|
|
|
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
|
|
|
|
gts.ExecAsync(func() {
|
|
|
|
c.UpdateMessageUnsafe(msg)
|
2020-06-13 21:51:03 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
|
|
|
gts.ExecAsync(func() {
|
2021-01-05 02:05:33 +00:00
|
|
|
msgID := msg.ID()
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// Get the previous and next message before deleting. We'll need them to
|
|
|
|
// evaluate whether we need to change anything.
|
2021-01-05 02:05:33 +00:00
|
|
|
prev, next := c.ListStore.Around(msgID)
|
2020-06-13 07:29:32 +00:00
|
|
|
|
|
|
|
// 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.
|
2021-01-05 02:05:33 +00:00
|
|
|
msg := c.ListStore.PopMessage(msgID)
|
2020-06-13 07:29:32 +00:00
|
|
|
|
|
|
|
// Don't calculate if we don't have any messages, or no messages before
|
|
|
|
// and after.
|
2021-01-02 09:24:14 +00:00
|
|
|
if c.ListStore.MessagesLen() == 0 || prev == nil || next == nil {
|
2020-06-13 07:29:32 +00:00
|
|
|
return
|
|
|
|
}
|
2020-06-07 04:27:28 +00:00
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
msgAuthorID := msg.Author().ID()
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// Check if the last message is the author's (relative to i):
|
2021-01-05 02:05:33 +00:00
|
|
|
if prev.Author().ID() == msgAuthorID {
|
2020-06-13 07:29:32 +00:00
|
|
|
// 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.
|
2021-01-05 02:05:33 +00:00
|
|
|
if next.Author().ID() != msgAuthorID {
|
2020-06-13 07:29:32 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-06-13 21:51:03 +00:00
|
|
|
// Uncompact or turn the message to a full one.
|
|
|
|
c.uncompact(next)
|
|
|
|
})
|
|
|
|
}
|
2020-06-13 07:29:32 +00:00
|
|
|
|
2021-01-02 09:24:14 +00:00
|
|
|
func (c *Container) uncompact(msg container.MessageRow) {
|
2020-06-13 21:51:03 +00:00
|
|
|
// We should only uncompact the message if it's compacted in the first
|
|
|
|
// place.
|
2021-01-05 02:05:33 +00:00
|
|
|
compact, ok := msg.(*CollapsedMessage)
|
2020-06-13 21:51:03 +00:00
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start the "lengthy" uncollapse process.
|
2021-01-05 02:05:33 +00:00
|
|
|
full := WrapFullMessage(compact.Unwrap())
|
2020-06-13 21:51:03 +00:00
|
|
|
// 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.
|
2021-01-05 02:05:33 +00:00
|
|
|
author := msg.Author()
|
|
|
|
c.reuseAvatar(author.ID(), author.Avatar(), full)
|
2020-06-13 21:51:03 +00:00
|
|
|
|
|
|
|
// Swap the old next message out for a new one.
|
2021-01-02 09:24:14 +00:00
|
|
|
c.ListStore.SwapMessage(full)
|
2020-06-06 07:44:36 +00:00
|
|
|
}
|
2020-08-20 23:53:13 +00:00
|
|
|
|
2021-01-02 09:24:14 +00:00
|
|
|
func (c *Container) compact(msg container.MessageRow) {
|
2021-01-05 02:05:33 +00:00
|
|
|
full, ok := msg.(*FullMessage)
|
2020-08-20 23:53:13 +00:00
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-01-05 02:05:33 +00:00
|
|
|
compact := WrapCollapsedMessage(full.Unwrap())
|
2020-08-20 23:53:13 +00:00
|
|
|
message.RefreshContainer(compact, compact.GenericContainer)
|
|
|
|
|
2021-01-02 09:24:14 +00:00
|
|
|
c.ListStore.SwapMessage(compact)
|
2020-08-20 23:53:13 +00:00
|
|
|
}
|