diff --git a/go.mod b/go.mod
index d96b7bd..8d6d667 100644
--- a/go.mod
+++ b/go.mod
@@ -4,8 +4,8 @@ go 1.14
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20201230071527-a77c32eb3876
-// replace github.com/diamondburned/gotk3-tcmalloc => ../../gotk3-tcmalloc
// replace github.com/diamondburned/cchat-discord => ../cchat-discord
+// replace github.com/diamondburned/gotk3-tcmalloc => ../../gotk3-tcmalloc
// replace github.com/diamondburned/ningen/v2 => ../../ningen
// replace github.com/diamondburned/arikawa/v2 => ../../arikawa
@@ -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.15
- github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092
+ github.com/diamondburned/cchat-discord v0.0.0-20201231025836-96e97aa11705
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 e429954..f798526 100644
--- a/go.sum
+++ b/go.sum
@@ -66,6 +66,8 @@ github.com/diamondburned/cchat-discord v0.0.0-20201227023505-c4e360010fb8 h1:eyK
github.com/diamondburned/cchat-discord v0.0.0-20201227023505-c4e360010fb8/go.mod h1:i3y8dyAFrtigpGOwunBdoJK/phwt9Gp/wfpVJb4imV0=
github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092 h1:oxY7APUclLgaWjaTK++7kHBdl0GdVyqOvHQv68TcpHw=
github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092/go.mod h1:rFBGZYLq0g6Pb/WGN/K0++kXrhCYlQQ1nc2FX4r8CO0=
+github.com/diamondburned/cchat-discord v0.0.0-20201231025836-96e97aa11705 h1:g0hwnUpeJ3yo7WaVZjWBQw875tnKVjCz4YofnamE9Fg=
+github.com/diamondburned/cchat-discord v0.0.0-20201231025836-96e97aa11705/go.mod h1:rFBGZYLq0g6Pb/WGN/K0++kXrhCYlQQ1nc2FX4r8CO0=
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/cozy/cozy.go b/internal/ui/messages/container/cozy/cozy.go
index ebdc47a..bc79a82 100644
--- a/internal/ui/messages/container/cozy/cozy.go
+++ b/internal/ui/messages/container/cozy/cozy.go
@@ -1,9 +1,6 @@
package cozy
import (
- "context"
- "runtime/pprof"
-
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
@@ -85,7 +82,7 @@ func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
// We can do the check here since we're never using NewPresendMessage for
// backlog messages.
- if c.lastMessageIsAuthor(msg.AuthorID(), 0) {
+ if c.lastMessageIsAuthor(msg.AuthorID(), msg.Author().String(), 0) {
return NewCollapsedSendingMessage(msg)
}
@@ -123,53 +120,52 @@ func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) {
}
}
-func (c *Container) lastMessageIsAuthor(id string, offset int) bool {
+func (c *Container) lastMessageIsAuthor(id cchat.ID, name string, offset int) bool {
// Get the offfsetth message from last.
var last = c.GridStore.NthMessage((c.GridStore.MessagesLen() - 1) + offset)
- return last != nil && last.AuthorID() == id
+ return gridMessageIsAuthor(last, id, name)
}
-var createMessageLabel = pprof.Labels("cozy", "createMessage")
+func gridMessageIsAuthor(gridMsg container.GridMessage, 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() {
- pprof.Do(context.Background(), createMessageLabel, func(context.Context) {
+ // Create the message in the parent's handler. This handler will also
+ // wipe old messages.
+ c.GridContainer.CreateMessageUnsafe(msg)
- // Create the message in the parent's handler. This handler will also
- // wipe old messages.
- c.GridContainer.CreateMessageUnsafe(msg)
+ // Did the handler wipe old messages? It will only do so if the user is
+ // scrolled to the bottom.
+ if c.GridContainer.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())
+ }
- // Did the handler wipe old messages? It will only do so if the user is
- // scrolled to the bottom.
- if c.GridContainer.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.GridContainer.LastMessage().ID():
+ author := msg.Author()
+ if c.lastMessageIsAuthor(author.ID(), author.Name().String(), -1) {
+ c.compact(c.GridContainer.LastMessage())
}
- 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.GridContainer.LastMessage().ID():
- if c.lastMessageIsAuthor(msg.Author().ID(), -1) {
- c.compact(c.GridContainer.LastMessage())
- }
-
- // If we've prepended the message, then see if we need to collapse the
- // second message.
- case c.GridContainer.FirstMessage().ID():
- if sec := c.NthMessage(1); sec != nil {
- // If the author isn't the same, then ignore.
- if sec.AuthorID() != msg.Author().ID() {
- return
- }
-
- // The author is the same; collapse.
+ // If we've prepended the message, then see if we need to collapse the
+ // second message.
+ case c.GridContainer.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)
}
}
-
- })
+ }
})
}
diff --git a/internal/ui/messages/container/cozy/message_collapsed.go b/internal/ui/messages/container/cozy/message_collapsed.go
index 4cf14a5..8ef09ef 100644
--- a/internal/ui/messages/container/cozy/message_collapsed.go
+++ b/internal/ui/messages/container/cozy/message_collapsed.go
@@ -34,6 +34,8 @@ func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
// Set Content's padding accordingly to FullMessage's main box.
gc.Content.ToWidget().SetMarginEnd(container.ColumnSpacing * 2)
+ gc.Username.SetMaxWidthChars(30)
+
return &CollapsedMessage{
GenericContainer: gc,
}
diff --git a/internal/ui/messages/container/cozy/message_full.go b/internal/ui/messages/container/cozy/message_full.go
index f5ed3a5..c081e22 100644
--- a/internal/ui/messages/container/cozy/message_full.go
+++ b/internal/ui/messages/container/cozy/message_full.go
@@ -78,6 +78,8 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
gc.Timestamp.SetVAlign(gtk.ALIGN_END) // bottom-align
gc.Timestamp.SetMarginStart(0) // clear margins
+ gc.Username.SetMaxWidthChars(75)
+
// Attach the class and CSS for the left avatar.
avatarCSS(avatar)
diff --git a/internal/ui/messages/memberlist/memberlist.go b/internal/ui/messages/memberlist/memberlist.go
index cf4d917..d0e9188 100644
--- a/internal/ui/messages/memberlist/memberlist.go
+++ b/internal/ui/messages/memberlist/memberlist.go
@@ -364,6 +364,10 @@ func NewMember(member cchat.ListMember) *Member {
return m
}
+var noMentionLinks = markup.RenderConfig{
+ NoMentionLinks: true,
+}
+
func (m *Member) Update(member cchat.ListMember) {
m.ListBoxRow.SetName(member.Name().Content)
@@ -371,7 +375,7 @@ func (m *Member) Update(member cchat.ListMember) {
m.Avatar.AsyncSetIconer(iconer, "Failed to get member list icon")
}
- m.output = markup.RenderCmplxWithConfig(member.Name(), markup.NoMentionLinks)
+ m.output = markup.RenderCmplxWithConfig(member.Name(), noMentionLinks)
txt := strings.Builder{}
txt.WriteString(fmt.Sprintf(
`● %s`,
diff --git a/internal/ui/messages/message/message.go b/internal/ui/messages/message/message.go
index 03e9a73..7f20562 100644
--- a/internal/ui/messages/message/message.go
+++ b/internal/ui/messages/message/message.go
@@ -19,6 +19,7 @@ type Container interface {
ID() string
Time() time.Time
AuthorID() string
+ AuthorName() string
AvatarURL() string // avatar
Nonce() string
@@ -47,11 +48,12 @@ func RefreshContainer(c Container, gc *GenericContainer) {
// GenericContainer provides a single generic message container for subpackages
// to use.
type GenericContainer struct {
- id string
- time time.Time
- authorID string
- avatarURL string // avatar
- nonce string
+ id string
+ time time.Time
+ authorID string
+ authorName string
+ avatarURL string // avatar
+ nonce string
Timestamp *gtk.Label
Username *labeluri.Label
@@ -94,7 +96,6 @@ func NewEmptyContainer() *GenericContainer {
ts.Show()
user := labeluri.NewLabel(text.Rich{})
- user.SetMaxWidthChars(35)
user.SetLineWrap(true)
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
user.SetXAlign(1) // right align
@@ -168,6 +169,10 @@ func (m *GenericContainer) AuthorID() string {
return m.authorID
}
+func (m *GenericContainer) AuthorName() string {
+ return m.authorName
+}
+
func (m *GenericContainer) AvatarURL() string {
return m.avatarURL
}
@@ -189,8 +194,11 @@ func (m *GenericContainer) UpdateAuthor(author cchat.Author) {
}
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
- var out = markup.RenderCmplxWithConfig(name, markup.NoMentionLinks)
- m.Username.SetOutput(out)
+ cfg := markup.RenderConfig{}
+ cfg.SetForegroundAnchor(m.ContentBody)
+
+ m.authorName = name.String()
+ m.Username.SetOutput(markup.RenderCmplxWithConfig(name, cfg))
}
func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
diff --git a/internal/ui/rich/parser/attrmap/attrmap.go b/internal/ui/rich/parser/attrmap/attrmap.go
index 70208b7..d7e205c 100644
--- a/internal/ui/rich/parser/attrmap/attrmap.go
+++ b/internal/ui/rich/parser/attrmap/attrmap.go
@@ -1,6 +1,7 @@
package attrmap
import (
+ "bytes"
"fmt"
"html"
"sort"
@@ -10,15 +11,15 @@ import (
)
type AppendMap struct {
- appended map[int]string // for opening tags
- prepended map[int]string // for closing tags
+ appended map[int][]byte // for opening tags
+ prepended map[int][]byte // for closing tags
indices []int
}
func NewAppendedMap() AppendMap {
return AppendMap{
- appended: map[int]string{},
- prepended: map[int]string{},
+ appended: map[int][]byte{},
+ prepended: map[int][]byte{},
indices: []int{},
}
}
@@ -40,7 +41,7 @@ func (a *AppendMap) Anchor(start, end int, href string) {
// AnchorNU makes a new tag without underlines and colors.
func (a *AppendMap) AnchorNU(start, end int, href string) {
- a.Openf(start, ``)
+ a.Openf(start, ``)
a.Close(end, "")
// a.Anchor(start, end, href)
a.Span(start, end, `underline="none"`)
@@ -63,7 +64,7 @@ func (a *AppendMap) Pad(start, end int) {
}
}
-func posHaveSpace(tags map[int]string, pos int) bool {
+func posHaveSpace(tags map[int][]byte, pos int) bool {
tg, ok := tags[pos]
if !ok || len(tg) == 0 {
return false
@@ -78,7 +79,7 @@ func posHaveSpace(tags map[int]string, pos int) bool {
}
// Check spaces mid-tag. This works because strings are always escaped.
- return strings.Contains(tg, "> <")
+ return bytes.Contains(tg, []byte("> <"))
}
func (a *AppendMap) Pair(start, end int, open, close string) {
@@ -92,32 +93,31 @@ func (a *AppendMap) Openf(ind int, f string, argv ...interface{}) {
func (a *AppendMap) Open(ind int, attr string) {
if str, ok := a.appended[ind]; ok {
- a.appended[ind] = str + attr // append
+ a.appended[ind] = append(str, []byte(attr)...) // append
return
}
- a.appended[ind] = attr
+ a.appended[ind] = []byte(attr)
a.appendIndex(ind)
}
func (a *AppendMap) Close(ind int, attr string) {
if str, ok := a.prepended[ind]; ok {
- a.prepended[ind] = attr + str // prepend
+ a.prepended[ind] = append([]byte(attr), str...) // prepend
return
}
- a.prepended[ind] = attr
+ a.prepended[ind] = []byte(attr)
a.appendIndex(ind)
}
func (a AppendMap) Get(ind int) (tags string) {
- if t, ok := a.appended[ind]; ok {
- tags += t
- }
- if t, ok := a.prepended[ind]; ok {
- tags += t
- }
- return
+ appended := a.appended[ind]
+ prepended := a.prepended[ind]
+
+ // Borrowing appended's backing array to add prepended is probably fine, as
+ // the length of the actual appended slice is going to stay the same.
+ return string(append(appended, prepended...))
}
func (a *AppendMap) Finalize(strlen int) []int {
diff --git a/internal/ui/rich/parser/hl/hl.go b/internal/ui/rich/parser/hl/hl.go
index 893a085..f7fbe20 100644
--- a/internal/ui/rich/parser/hl/hl.go
+++ b/internal/ui/rich/parser/hl/hl.go
@@ -1,3 +1,4 @@
+// Package hl provides a syntax highlighted renderer for the markup API.
package hl
import (
diff --git a/internal/ui/rich/parser/markup/markup.go b/internal/ui/rich/parser/markup/markup.go
index 0fc6bd0..f68d287 100644
--- a/internal/ui/rich/parser/markup/markup.go
+++ b/internal/ui/rich/parser/markup/markup.go
@@ -9,10 +9,12 @@ import (
"strconv"
"strings"
+ "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/hl"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
+ "github.com/gotk3/gotk3/gtk"
)
// Hyphenate controls whether or not texts should have hyphens on wrap.
@@ -24,9 +26,10 @@ func hyphenate(text string) string {
// RenderOutput is the output of a render.
type RenderOutput struct {
- Markup string
- Input string // useless to keep parts, as Go will keep all alive anyway
- Mentions []MentionSegment
+ Markup string
+ Input string // useless to keep parts, as Go will keep all alive anyway
+ Mentions []MentionSegment
+ References []ReferenceSegment
}
// MentionSegment is a type that satisfies both Segment and Mentioner.
@@ -35,24 +38,41 @@ type MentionSegment struct {
text.Mentioner
}
-// f_Mention is used to print and parse mention URIs.
-const f_Mention = "cchat://mention/%d" // %d == Mentions[i]
+// ReferenceSegment is a type that satisfies both Segment and MessageReferencer.
+type ReferenceSegment struct {
+ text.Segment
+ text.MessageReferencer
+}
+
+const (
+ // f_Mention is used to print and parse mention URIs.
+ f_Mention = "cchat://mention/%d" // %d == Mentions[i]
+ f_Reference = "cchat://reference/%d" // %d == References[i]
+)
// IsMention returns the mention if the URI is correct, or nil if none.
func (r RenderOutput) IsMention(uri string) text.Segment {
var i int
- if _, err := fmt.Sscanf(uri, f_Mention, &i); err != nil {
- return nil
- }
-
- if i >= len(r.Mentions) {
+ _, err := fmt.Sscanf(uri, f_Mention, &i)
+ if err != nil || i >= len(r.Mentions) {
return nil
}
return r.Mentions[i]
}
+func (r RenderOutput) IsReference(uri string) text.Segment {
+ var i int
+
+ _, err := fmt.Sscanf(uri, f_Reference, &i)
+ if err != nil || i >= len(r.References) {
+ return nil
+ }
+
+ return r.References[i]
+}
+
func Render(content text.Rich) string {
return RenderCmplx(content).Markup
}
@@ -63,15 +83,32 @@ func RenderCmplx(content text.Rich) RenderOutput {
}
type RenderConfig struct {
- // NoMentionLinks prevents the renderer from wrapping mentions with a
- // hyperlink. This prevents invalid colors.
+ // NoMentionLinks, if true, will not render any mentions.
NoMentionLinks bool
+
+ // AnchorColor forces all anchors to be of a certain color. This is used if
+ // the boolean is true. Else, all mention links will not work and regular
+ // links will be of the default color.
+ AnchorColor struct {
+ uint32
+ bool
+ }
}
-// NoMentionLinks is the config to render author names. It disables author
-// mention links, as there's no way to make normal names not appear blue.
-var NoMentionLinks = RenderConfig{
- NoMentionLinks: true,
+// SetForegroundAnchor sets the AnchorColor of the render config to be that of
+// the regular text foreground color.
+func (c *RenderConfig) SetForegroundAnchor(styler primitives.StyleContexter) {
+ styleCtx, _ := styler.GetStyleContext()
+
+ if rgba := styleCtx.GetColor(gtk.STATE_FLAG_NORMAL); rgba != nil {
+ var color uint32
+ for _, v := range rgba.Floats() { // [0.0, 1.0]
+ color = (color << 8) + uint32(v*0xFF)
+ }
+
+ c.AnchorColor.bool = true
+ c.AnchorColor.uint32 = color
+ }
}
func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
@@ -104,15 +141,21 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
// map to append strings to indices
var appended = attrmap.NewAppendedMap()
- // map to store mentions
+ // map to store mentions and references
var mentions []MentionSegment
+ var references []ReferenceSegment
// Parse all segments.
for _, segment := range content.Segments {
start, end := segment.Bounds()
+ // hasAnchor is used to determine if the current segment has inserted
+ // any anchor tags; it is used for AnchorColor.
+ var hasAnchor bool
+
if linker := segment.AsLinker(); linker != nil {
appended.Anchor(start, end, linker.Link())
+ hasAnchor = true
}
// Only inline images if start == end per specification.
@@ -127,19 +170,14 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
}
}
- if colorer := segment.AsColorer(); colorer != nil {
- appended.Span(start, end, colorAttrs(colorer.Color(), false)...)
- }
-
// Mentioner needs to be before colorer, as we'd want the below color
// segment to also highlight the full mention as well as make the
// padding part of the hyperlink.
- if mentioner := segment.AsMentioner(); mentioner != nil {
+ if mentioner := segment.AsMentioner(); mentioner != nil && !cfg.NoMentionLinks {
// Render the mention into "cchat://mention:0" or such. Other
// components will take care of showing the information.
- if !cfg.NoMentionLinks {
- appended.AnchorNU(start, end, fmt.Sprintf(f_Mention, len(mentions)))
- }
+ appended.AnchorNU(start, end, fmt.Sprintf(f_Mention, len(mentions)))
+ hasAnchor = true
// Add the mention segment into the list regardless of hyperlinks.
mentions = append(mentions, MentionSegment{
@@ -147,15 +185,44 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
Mentioner: mentioner,
})
- if colorer := segment.AsColorer(); colorer != nil {
- // Only pad the name and add a dimmed background if the bounds
- // do not cover the whole segment.
- var cover = (start == 0) && (end == len(content.Content))
- appended.Span(start, end, colorAttrs(colorer.Color(), !cover)...)
- if !cover {
- appended.Pad(start, end)
- }
- }
+ // TODO: figure out a way to readd Pad. Right now, backend
+ // implementations can arbitrarily add multiple mentions onto the
+ // author for overloading, which we don't want to break.
+
+ // // Determine if the mention segment covers the entire label.
+ // // Only pad the name and add a dimmed background if the bounds do
+ // // not cover the whole segment.
+ // var cover = (start == 0) && (end == len(content.Content))
+ // if !cover {
+ // appended.Pad(start, end)
+ // }
+
+ // // If we don't have a mention color for this segment, then try to
+ // // use our own AnchorColor.
+ // if !hasColor && cfg.AnchorColor.bool {
+ // appended.Span(start, end, colorAttrs(cfg.AnchorColor.uint32, false)...)
+ // }
+ }
+
+ if colorer := segment.AsColorer(); colorer != nil {
+ appended.Span(start, end, colorAttrs(colorer.Color(), false)...)
+ } else if hasAnchor && cfg.AnchorColor.bool {
+ appended.Span(start, end, colorAttrs(cfg.AnchorColor.uint32, false)...)
+ }
+
+ // Don't use AnchorColor for the link, as we're technically just
+ // borrowing the anchor tag for its use. We should also prefer the
+ // username popover (Mention) over this.
+ if reference := segment.AsMessageReferencer(); !hasAnchor && reference != nil {
+ // Render the mention into "cchat://reference:0" or such. Other
+ // components will take care of showing the information.
+ appended.AnchorNU(start, end, fmt.Sprintf(f_Reference, len(references)))
+
+ // Add the mention segment into the list regardless of hyperlinks.
+ references = append(references, ReferenceSegment{
+ Segment: segment,
+ MessageReferencer: reference,
+ })
}
if attributor := segment.AsAttributor(); attributor != nil {
@@ -165,7 +232,12 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
if codeblocker := segment.AsCodeblocker(); codeblocker != nil {
start, end := segment.Bounds()
// Syntax highlight the codeblock.
- hl.Segments(&appended, content.Content, start, end, codeblocker.CodeblockLanguage())
+ hl.Segments(
+ &appended,
+ content.Content,
+ start, end,
+ codeblocker.CodeblockLanguage(),
+ )
}
// TODO: make this not shit. Maybe make it somehow not rely on green
@@ -187,9 +259,10 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
}
return RenderOutput{
- Markup: hyphenate(buf.String()),
- Input: content.Content,
- Mentions: mentions,
+ Markup: hyphenate(buf.String()),
+ Input: content.Content,
+ Mentions: mentions,
+ References: references,
}
}