bumped discord; partial message reference support

This commit is contained in:
diamondburned 2020-12-30 19:00:00 -08:00
parent 744f59cf38
commit c8f5446710
10 changed files with 192 additions and 104 deletions

4
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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)
}
}
})
}
})
}

View File

@ -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,
}

View File

@ -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)

View File

@ -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(
`<span color="#%06X">●</span> %s`,

View File

@ -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) {

View File

@ -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 <a> tag without underlines and colors.
func (a *AppendMap) AnchorNU(start, end int, href string) {
a.Openf(start, `<a href="`+html.EscapeString(href)+`%s">`)
a.Openf(start, `<a href="`+html.EscapeString(href)+`">`)
a.Close(end, "</a>")
// 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 {

View File

@ -1,3 +1,4 @@
// Package hl provides a syntax highlighted renderer for the markup API.
package hl
import (

View File

@ -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,
}
}