diff --git a/go.mod b/go.mod index 20bd9bd..7ea33ab 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/Xuanwo/go-locale v1.0.0 github.com/alecthomas/chroma v0.7.3 github.com/diamondburned/cchat v0.3.17 - github.com/diamondburned/cchat-discord v0.0.0-20210106052627-13f87a764b33 + github.com/diamondburned/cchat-discord v0.0.0-20210107010729-8edbbcc24992 github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374 diff --git a/go.sum b/go.sum index 2fa30e4..ef076d7 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/diamondburned/cchat-discord v0.0.0-20210106050254-2c7b56ab6e00 h1:4LO github.com/diamondburned/cchat-discord v0.0.0-20210106050254-2c7b56ab6e00/go.mod h1:YjAiN/4zl5Z84Atsjw1h7G3wjcFxnXwk/qIYyLCjII8= github.com/diamondburned/cchat-discord v0.0.0-20210106052627-13f87a764b33 h1:Pl7cAMfz20UamYIeIKNFsD/GdgttEdQWCViEGVFqIgw= github.com/diamondburned/cchat-discord v0.0.0-20210106052627-13f87a764b33/go.mod h1:g+RqSLt/ccZZVZbsukOiCwQCqKM/9HfHMJVboIUW+tU= +github.com/diamondburned/cchat-discord v0.0.0-20210107010729-8edbbcc24992 h1:njLrtjrCpaU/AVVj5TS7mhuDrm60f8CBFqvDFdpH4uI= +github.com/diamondburned/cchat-discord v0.0.0-20210107010729-8edbbcc24992/go.mod h1:g+RqSLt/ccZZVZbsukOiCwQCqKM/9HfHMJVboIUW+tU= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc= github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw= diff --git a/internal/ui/messages/container/container.go b/internal/ui/messages/container/container.go index 5079637..f8aa239 100644 --- a/internal/ui/messages/container/container.go +++ b/internal/ui/messages/container/container.go @@ -55,6 +55,8 @@ type Container interface { LatestMessageFrom(authorID string) (msgID string, ok bool) // Message finds and returns the message, if any. Message(id cchat.ID, nonce string) MessageRow + // FindMessage finds a message that satisfies the given callback. + FindMessage(isMessage func(MessageRow) bool) MessageRow // Highlight temporarily highlights the given message for a short while. Highlight(msg MessageRow) diff --git a/internal/ui/messages/container/cozy/cozy.go b/internal/ui/messages/container/cozy/cozy.go index 92c1f6a..906b3cc 100644 --- a/internal/ui/messages/container/cozy/cozy.go +++ b/internal/ui/messages/container/cozy/cozy.go @@ -1,6 +1,8 @@ package cozy import ( + "time" + "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/ui/messages/container" @@ -49,7 +51,7 @@ var messageConstructors = container.Constructor{ func NewMessage( msg cchat.MessageCreate, before container.MessageRow) container.MessageRow { - if gridMessageIsAuthor(before, msg.Author()) { + if isCollapsible(before, msg) { return NewCollapsedMessage(msg) } @@ -59,7 +61,7 @@ func NewMessage( func NewPresendMessage( msg input.PresendMessage, before container.MessageRow) container.PresendMessageRow { - if gridMessageIsAuthor(before, msg.Author()) { + if isCollapsible(before, msg) { return NewCollapsedSendingMessage(msg) } @@ -105,14 +107,34 @@ func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) { // lastMessageIsAuthor removed - assuming index before insertion is harmful. -func gridMessageIsAuthor(gridMsg container.MessageRow, author cchat.Author) bool { - if gridMsg == nil { +type authoredMessage interface { + cchat.MessageHeader + Author() cchat.Author +} + +var ( + _ authoredMessage = (cchat.MessageCreate)(nil) + _ authoredMessage = (input.PresendMessage)(nil) + _ authoredMessage = (container.MessageRow)(nil) + _ authoredMessage = (container.PresendMessageRow)(nil) +) + +const splitDuration = 10 * time.Minute + +// isCollapsible returns true if the given lastMsg has matching conditions with +// the given msg. +func isCollapsible(lastMsg container.MessageRow, msg authoredMessage) bool { + if lastMsg == nil || msg == nil { return false } - leftAuthor := gridMsg.Author() + + lastAuthor := lastMsg.Author() + thisAuthor := msg.Author() + return true && - leftAuthor.ID() == author.ID() && - leftAuthor.Name().String() == author.Name().String() + lastAuthor.ID() == thisAuthor.ID() && + lastAuthor.Name().String() == thisAuthor.Name().String() && + lastMsg.Time().Add(splitDuration).After(msg.Time()) } func (c *Container) CreateMessage(msg cchat.MessageCreate) { @@ -147,7 +169,7 @@ func (c *Container) CreateMessage(msg cchat.MessageCreate) { // second message. 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()) { + if sec := c.NthMessage(1); sec != nil && isCollapsible(sec, msg) { c.compact(sec) } } diff --git a/internal/ui/messages/container/list.go b/internal/ui/messages/container/list.go index cb7b399..0c28aa0 100644 --- a/internal/ui/messages/container/list.go +++ b/internal/ui/messages/container/list.go @@ -1,6 +1,8 @@ package container import ( + "log" + "strings" "time" "github.com/diamondburned/cchat" @@ -19,6 +21,44 @@ type messageKey struct { func nonceKey(nonce string) messageKey { return messageKey{nonce, true} } func idKey(id cchat.ID) messageKey { return messageKey{id, false} } +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 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; } `) @@ -59,9 +99,9 @@ func NewListStore(ctrl Controller, constr Constructor) *ListStore { return } - id, _ := r.GetName() + id := parseKeyFromNamer(r) - msg := listStore.Message(id, "") + msg := listStore.Message(id.expand()) if msg == nil { return } @@ -134,18 +174,18 @@ func (c *ListStore) around(aroundID cchat.ID) (before, after *messageRow) { var next bool primitives.ForeachChildBackwards(c.ListBox, func(v interface{}) (stop bool) { - id := primitives.GetName(v.(primitives.Namer)) + id := parseKeyFromNamer(v.(primitives.Namer)) if next { - after = c.message(id, "") + after = c.message(id.expand()) return true } - if id == aroundID { + if !id.nonce && id.id == aroundID { before = last next = true return false } - last = c.message(id, "") + last = c.message(id.expand()) return false }) @@ -176,9 +216,9 @@ func (c *ListStore) findIndex(findID cchat.ID) (found *messageRow, index int) { index = c.MessagesLen() - 1 primitives.ForeachChildBackwards(c.ListBox, func(v interface{}) (stop bool) { - id := primitives.GetName(v.(primitives.Namer)) - if id == findID { - found = c.message(findID, "") + id := parseKeyFromNamer(v.(primitives.Namer)) + if !id.nonce && id.id == findID { + found = c.message(id.expand()) return true } @@ -201,8 +241,8 @@ func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messa var i = c.MessagesLen() - 1 primitives.ForeachChildBackwards(c.ListBox, func(v interface{}) (stop bool) { - id := primitives.GetName(v.(primitives.Namer)) - gridMsg := c.message(id, "") + id := parseKeyFromNamer(v.(primitives.Namer)) + gridMsg := c.message(id.expand()) // If gridMsg is actually nil, then we have bigger issues. if gridMsg != nil { @@ -240,8 +280,8 @@ func (c *ListStore) nthMessage(n int) *messageRow { return nil } - id := primitives.GetName(v.(primitives.Namer)) - return c.message(id, "") + id := parseKeyFromNamer(v.(primitives.Namer)) + return c.message(id.expand()) } // NthMessage returns the nth message. @@ -280,6 +320,7 @@ func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow { // 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) @@ -301,12 +342,34 @@ func (c *ListStore) ensureEmpty() { } } +func (c *ListStore) bindMessage(msgc *messageRow) { + // Bind the message ID to the row so we can easily do a lookup. + var key messageKey + if id := msgc.ID(); id != "" { + key.id = id + } else { + key.id = msgc.Nonce() + key.nonce = true + } + + msgc.Row().SetName(key.name()) + msgc.SetReferenceHighlighter(c) + c.Controller.BindMenu(msgc.MessageRow) +} + // AddPresendMessage inserts an input.PresendMessage into the container and // returning a wrapped widget interface. func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRow { c.ensureEmpty() before := c.LastMessage() + + if before != nil { + log.Println("Found before:", before.Author().Name()) + } else { + log.Println("Before is nil") + } + presend := c.Construct.NewPresendMessage(msg, before) msgc := &messageRow{ @@ -319,14 +382,9 @@ func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRo // Set the NONCE into the message map. c.messages[nonceKey(msgc.Nonce())] = msgc - return presend -} + c.bindMessage(msgc) -func (c *ListStore) bindMessage(msgc *messageRow) { - // Bind the message ID to the row so we can easily do a lookup. - msgc.Row().SetName(msgc.ID()) - msgc.SetReferenceHighlighter(c) - c.Controller.BindMenu(msgc.MessageRow) + return presend } // Many attempts were made to have CreateMessageUnsafe return an index. That is @@ -436,8 +494,8 @@ func (c *ListStore) DeleteEarliest(n int) { // 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 := primitives.GetName(v.(primitives.Namer)) - gridMsg := c.message(id, "") + id := parseKeyFromNamer(v.(primitives.Namer)) + gridMsg := c.message(id.expand()) if id := gridMsg.ID(); id != "" { delete(c.messages, idKey(id)) diff --git a/internal/ui/messages/input/input.go b/internal/ui/messages/input/input.go index 084a016..225e181 100644 --- a/internal/ui/messages/input/input.go +++ b/internal/ui/messages/input/input.go @@ -22,6 +22,7 @@ type Controller interface { AddPresendMessage(msg PresendMessage) (onErr func(error)) LatestMessageFrom(userID cchat.ID) (messageID cchat.ID, ok bool) MessageAuthor(msgID cchat.ID) cchat.Author + Author(authorID cchat.ID) cchat.Author } // LabelBorrower is an interface that allows the caller to borrow a label. diff --git a/internal/ui/messages/input/sendable.go b/internal/ui/messages/input/sendable.go index 5626742..5113cb8 100644 --- a/internal/ui/messages/input/sendable.go +++ b/internal/ui/messages/input/sendable.go @@ -65,7 +65,7 @@ func (f *Field) sendInput() { // Derive the author. Prefer the author of the current user from the message // buffer over the one in the username feed, unless we can't find any. - var author cchat.Author = f.ctrl.MessageAuthor(f.UserID) + var author = f.ctrl.Author(f.UserID) if author == nil { author = newAuthor(f) } diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go index 6ae5f86..0dadc19 100644 --- a/internal/ui/messages/view.go +++ b/internal/ui/messages/view.go @@ -397,6 +397,7 @@ func (v *View) AuthorEvent(author cchat.Author) { } } +// MessageAuthor returns the author from the message with the given ID. func (v *View) MessageAuthor(msgID cchat.ID) cchat.Author { msg := v.Container.Message(msgID, "") if msg == nil { @@ -406,6 +407,18 @@ func (v *View) MessageAuthor(msgID cchat.ID) cchat.Author { return msg.Author() } +// Author returns the author from the message list with the given author ID. +func (v *View) Author(authorID cchat.ID) cchat.Author { + msg := v.Container.FindMessage(func(msg container.MessageRow) bool { + return msg.Author().ID() == authorID + }) + if msg == nil { + return nil + } + + return msg.Author() +} + // LatestMessageFrom returns the last message ID with that author. func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) { return v.Container.LatestMessageFrom(userID) diff --git a/internal/ui/primitives/primitives.go b/internal/ui/primitives/primitives.go index 7217ace..2634ad2 100644 --- a/internal/ui/primitives/primitives.go +++ b/internal/ui/primitives/primitives.go @@ -57,13 +57,11 @@ func NthChild(w Container, n int) interface{} { children := w.GetChildren() defer children.Free() - // Bound check! - if n < 0 || int(children.Length()) >= n { - return nil - } + length := int(children.Length()) - if n == 0 { - return children.Data() + // Bound check! + if !(0 <= n && n < length) { + return nil } return children.NthData(uint(n))