package cozy import ( "time" "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/message" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" ) // 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 ) // NewMessage creates a new message. func NewMessage( s *message.State, before container.MessageRow) container.MessageRow { if isCollapsible(before, s) { return WrapCollapsedMessage(s) } return WrapFullMessage(s) } // NewPresendMessage creates a new presend message. func NewPresendMessage( s *message.PresendState, before container.MessageRow) container.PresendMessageRow { if isCollapsible(before, s.State) { return WrapCollapsedSendingMessage(s) } return WrapFullSendingMessage(s) } type Container struct { *container.ListContainer } func NewContainer(ctrl container.Controller) *Container { c := container.NewListContainer(ctrl) primitives.AddClass(c, "cozy-container") return &Container{ListContainer: c} } 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.Unwrap(false).Author.ID == authorID }) } const splitDuration = 10 * time.Minute // isCollapsible returns true if the given lastMsg has matching conditions with // the given msg. func isCollapsible(last container.MessageRow, msg *message.State) bool { if last == nil || msg.ID == "" { return false } lastMsg := last.Unwrap(false) return true && lastMsg.Author.ID == msg.ID && lastMsg.Time.Add(splitDuration).After(msg.Time) } func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow { msgr := NewPresendMessage(state, c.LastMessage()) c.AddMessage(msgr) return msgr } func (c *Container) CreateMessage(msg cchat.MessageCreate) { gts.ExecAsync(func() { state := message.NewState(msg) msgr := NewMessage(state, c.LastMessage()) c.AddMessage(msgr) }) } // AddMessage adds the given message. func (c *Container) AddMessage(msgr container.MessageRow) { // Create the message in the parent's handler. This handler will also // wipe old messages. c.ListContainer.AddMessage(msgr) // 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()) } // If we've prepended the message, then see if we need to collapse the // second message. if first := c.ListContainer.FirstMessage(); first != nil { firstState := first.Unwrap(false) msgState := msgr.Unwrap(false) if firstState.ID == msgState.ID { // If the author is the same, then collapse. if sec := c.NthMessage(1); isCollapsible(sec, firstState) { c.compact(sec) } } } } func (c *Container) UpdateMessage(msg cchat.MessageUpdate) { gts.ExecAsync(func() { container.UpdateMessage(c, msg) }) } func (c *Container) DeleteMessage(msg cchat.MessageDelete) { gts.ExecAsync(func() { msgID := msg.ID() // 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(msgID) // 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(msgID) // 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 } msgHeader := msg.Unwrap(false) prevHeader := prev.Unwrap(false) nextHeader := next.Unwrap(false) // Check if the last message is the author's (relative to i): if prevHeader.Author.ID == msgHeader.Author.ID { // 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 nextHeader.Author.ID != msgHeader.Author.ID { return } // Uncompact or turn the message to a full one. c.uncompact(next) }) } func (c *Container) uncompact(msg container.MessageRow) { full := WrapFullMessage(msg.Unwrap(true)) c.ListStore.SwapMessage(full) } func (c *Container) compact(msg container.MessageRow) { compact := WrapCollapsedMessage(msg.Unwrap(true)) c.ListStore.SwapMessage(compact) }