diff --git a/internal/gts/gts.go b/internal/gts/gts.go
index f8e4daf..e1fbb8d 100644
--- a/internal/gts/gts.go
+++ b/internal/gts/gts.go
@@ -22,6 +22,13 @@ var App struct {
*gtk.Application
Window *handy.ApplicationWindow
Throttler *throttler.State
+
+ closing bool
+}
+
+// IsClosing returns true if the window is destroyed.
+func IsClosing() bool {
+ return App.closing
}
// Windower is the interface for a window.
@@ -121,6 +128,7 @@ func Main(wfn func() MainApplication) {
App.Window.Window.Connect("destroy", func(window *handy.ApplicationWindow) {
// Hide the application window.
window.Hide()
+ App.closing = true
// Let the main loop run once by queueing the stop loop afterwards.
// This is to allow the main loop to properly hide the Gtk window
diff --git a/internal/gts/httputil/httputil.go b/internal/gts/httputil/httputil.go
index b721d31..eb4dd11 100644
--- a/internal/gts/httputil/httputil.go
+++ b/internal/gts/httputil/httputil.go
@@ -13,22 +13,31 @@ import (
"github.com/pkg/errors"
)
-var basePath = filepath.Join(os.TempDir(), "cchat-gtk-totally-not-node-modules")
+var basePath = filepath.Join(os.TempDir(), "cchat-gtk-caching-is-hard")
var dskcached = http.Client{
Timeout: 15 * time.Second,
- Transport: httpcache.NewTransport(
- diskcache.NewWithDiskv(diskv.New(diskv.Options{
+ Transport: &httpcache.Transport{
+ Transport: &http.Transport{
+ // Be generous: use a 128KB buffer instead of 4KB to hopefully
+ // reduce cgo calls.
+ WriteBufferSize: 128 * 1024,
+ ReadBufferSize: 128 * 1024,
+ },
+ Cache: diskcache.NewWithDiskv(diskv.New(diskv.Options{
BasePath: basePath,
TempDir: filepath.Join(basePath, "tmp"),
PathPerm: 0750,
FilePerm: 0750,
- Compression: diskv.NewZlibCompressionLevel(5),
- CacheSizeMax: 0, // 25 MiB in memory
+ Compression: diskv.NewZlibCompressionLevel(4),
+ CacheSizeMax: 25 * 1024 * 1024, // 25 MiB in memory
})),
- ),
+ MarkCachedResponses: true,
+ },
}
+// TODO: log cache misses with httpcache.XFromCache
+
func get(ctx context.Context, url string, cached bool) (r *http.Response, err error) {
q, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
diff --git a/internal/gts/httputil/image.go b/internal/gts/httputil/image.go
index 0ded0e2..a90f2e3 100644
--- a/internal/gts/httputil/image.go
+++ b/internal/gts/httputil/image.go
@@ -1,6 +1,7 @@
package httputil
import (
+ "bufio"
"context"
"io"
"mime"
@@ -8,6 +9,7 @@ import (
"net/url"
"path"
"strings"
+ "sync"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
@@ -19,7 +21,31 @@ import (
"github.com/pkg/errors"
)
-// TODO:
+// bufferPool provides a sync.Pool of *bufio.Writers. This is used to reduce the
+// amount of cgo calls, by writing bytes in larger chunks.
+//
+// Technically, httpcache already wraps its cached reader around a bufio.Reader,
+// but we have no control over the buffer size.
+var bufferPool = sync.Pool{
+ New: func() interface{} {
+ // Allocate a 512KB buffer by default.
+ const defaultBufSz = 512 * 1024
+
+ return bufio.NewWriterSize(nil, defaultBufSz)
+ },
+}
+
+func bufferedWriter(w io.Writer) *bufio.Writer {
+ buf := bufferPool.Get().(*bufio.Writer)
+ buf.Reset(w)
+ return buf
+}
+
+func returnBufferedWriter(buf *bufio.Writer) {
+ // Unreference the internal reader.
+ buf.Reset(nil)
+ bufferPool.Put(buf)
+}
type ImageContainer interface {
primitives.Connector
@@ -67,9 +93,9 @@ func AsyncImage(ctx context.Context,
scale = surfaceContainer.GetScaleFactor()
}
- go func() {
- ctx := primitives.HandleDestroyCtx(ctx, img)
+ ctx = primitives.HandleDestroyCtx(ctx, img)
+ go func() {
// Try and guess the MIME type from the URL.
mimeType := mime.TypeByExtension(urlExt(imageURL))
@@ -102,7 +128,7 @@ func AsyncImage(ctx context.Context,
l, err := gdk.PixbufLoaderNewWithType(fileType)
if err != nil {
- log.Error(errors.Wrap(err, "failed to make pixbuf loader"))
+ log.Error(errors.Wrapf(err, "failed to make PixbufLoader type %q", fileType))
return
}
@@ -117,11 +143,20 @@ func AsyncImage(ctx context.Context,
l.Connect("area-prepared", load)
l.Connect("area-updated", load)
- if err := downloadImage(r.Body, l, procs, isGIF); err != nil {
+ // Borrow a buffered writer and return it at the end.
+ bufWriter := bufferedWriter(l)
+ defer returnBufferedWriter(bufWriter)
+
+ if err := downloadImage(r.Body, bufWriter, procs, isGIF); err != nil {
log.Error(errors.Wrapf(err, "failed to download %q", imageURL))
// Force close after downloading.
}
+ if err := bufWriter.Flush(); err != nil {
+ log.Error(errors.Wrapf(err, "failed to flush writer for %q", imageURL))
+ // Force close after downloading.
+ }
+
if err := l.Close(); err != nil {
log.Error(errors.Wrapf(err, "failed to close pixbuf loader for %q", imageURL))
}
diff --git a/internal/ui/messages/container/compact/compact.go b/internal/ui/messages/container/compact/compact.go
index ecdd173..91354bd 100644
--- a/internal/ui/messages/container/compact/compact.go
+++ b/internal/ui/messages/container/compact/compact.go
@@ -13,8 +13,8 @@ type Container struct {
}
func NewContainer(ctrl container.Controller) *Container {
- c := container.NewListContainer(constructor{}, ctrl)
- primitives.AddClass(c, "compact-conatainer")
+ c := container.NewListContainer(ctrl, constructors)
+ primitives.AddClass(c, "compact-container")
return &Container{c}
}
@@ -33,12 +33,19 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
gts.ExecAsync(func() { c.ListContainer.DeleteMessageUnsafe(msg) })
}
-type constructor struct{}
+var constructors = container.Constructor{
+ NewMessage: newMessage,
+ NewPresendMessage: newPresendMessage,
+}
+
+func newMessage(
+ msg cchat.MessageCreate, _ container.MessageRow) container.MessageRow {
-func (constructor) NewMessage(msg cchat.MessageCreate) container.MessageRow {
return NewMessage(msg)
}
-func (constructor) NewPresendMessage(msg input.PresendMessage) container.PresendMessageRow {
+func newPresendMessage(
+ msg input.PresendMessage, _ container.MessageRow) container.PresendMessageRow {
+
return NewPresendMessage(msg)
}
diff --git a/internal/ui/messages/container/compact/message.go b/internal/ui/messages/container/compact/message.go
index 1d8442f..e070650 100644
--- a/internal/ui/messages/container/compact/message.go
+++ b/internal/ui/messages/container/compact/message.go
@@ -1,15 +1,33 @@
package compact
import (
+ "time"
+
"github.com/diamondburned/cchat"
+ "github.com/diamondburned/cchat-gtk/internal/humanize"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
+ "github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
+ "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
+var messageTimeCSS = primitives.PrepareClassCSS("", `
+ .message-time {
+ margin-left: 1em;
+ margin-right: 1em;
+ }
+`)
+
+var messageAuthorCSS = primitives.PrepareClassCSS("", `
+ .message-author {
+ margin-right: 0.5em;
+ }
+`)
+
type PresendMessage struct {
message.PresendContainer
Message
@@ -17,60 +35,76 @@ type PresendMessage struct {
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
msgc := message.NewPresendContainer(msg)
- attachCompact(msgc.GenericContainer)
return PresendMessage{
PresendContainer: msgc,
- Message: Message{msgc.GenericContainer},
+ Message: wrapMessage(msgc.GenericContainer),
}
}
type Message struct {
*message.GenericContainer
+ Timestamp *gtk.Label
+ Username *labeluri.Label
}
var _ container.MessageRow = (*Message)(nil)
func NewMessage(msg cchat.MessageCreate) Message {
- msgc := message.NewContainer(msg)
- attachCompact(msgc)
+ msgc := wrapMessage(message.NewContainer(msg))
message.FillContainer(msgc, msg)
-
- return Message{msgc}
+ return msgc
}
func NewEmptyMessage() Message {
ct := message.NewEmptyContainer()
- attachCompact(ct)
-
- return Message{ct}
+ return wrapMessage(ct)
}
-var messageTimeCSS = primitives.PrepareClassCSS("message-time", `
- .message-time {
- margin-left: 1em;
- margin-right: 1em;
+func wrapMessage(ct *message.GenericContainer) Message {
+ ts := message.NewTimestamp()
+ ts.SetVAlign(gtk.ALIGN_START)
+ ts.Show()
+ messageTimeCSS(ts)
+
+ user := message.NewUsername()
+ user.SetMaxWidthChars(25)
+ user.SetEllipsize(pango.ELLIPSIZE_NONE)
+ user.SetLineWrap(true)
+ user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
+ user.Show()
+ messageAuthorCSS(user)
+
+ ct.PackStart(ts, false, false, 0)
+ ct.PackStart(user, false, false, 0)
+ ct.PackStart(ct.Content, true, true, 0)
+ ct.SetClass("compact")
+
+ return Message{
+ GenericContainer: ct,
+ Timestamp: ts,
+ Username: user,
}
-`)
-
-var messageAuthorCSS = primitives.PrepareClassCSS("message-author", `
- .message-author {
- margin-right: 0.5em;
- }
-`)
-
-func attachCompact(container *message.GenericContainer) {
- container.Timestamp.SetVAlign(gtk.ALIGN_START)
- container.Username.SetMaxWidthChars(25)
- container.Username.SetEllipsize(pango.ELLIPSIZE_NONE)
- container.Username.SetLineWrap(true)
- container.Username.SetLineWrapMode(pango.WRAP_WORD_CHAR)
-
- messageTimeCSS(container.Timestamp)
- messageAuthorCSS(container.Username)
-
- container.PackStart(container.Timestamp, false, false, 0)
- container.PackStart(container.Username, false, false, 0)
- container.PackStart(container.Content, true, true, 0)
- container.SetClass("compact")
+}
+
+// SetReferenceHighlighter sets the reference highlighter into the message.
+func (m Message) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
+ m.GenericContainer.SetReferenceHighlighter(r)
+ m.Username.SetReferenceHighlighter(r)
+}
+
+func (m Message) UpdateTimestamp(t time.Time) {
+ m.GenericContainer.UpdateTimestamp(t)
+ m.Timestamp.SetText(humanize.TimeAgo(t))
+ m.Timestamp.SetTooltipText(t.Format(time.Stamp))
+}
+
+func (m Message) UpdateAuthor(author cchat.Author) {
+ m.GenericContainer.UpdateAuthor(author)
+
+ cfg := markup.RenderConfig{}
+ cfg.NoReferencing = true
+ cfg.SetForegroundAnchor(m.ContentBodyStyle)
+
+ m.Username.SetOutput(markup.RenderCmplxWithConfig(author.Name(), cfg))
}
diff --git a/internal/ui/messages/container/container.go b/internal/ui/messages/container/container.go
index 0251a8a..5079637 100644
--- a/internal/ui/messages/container/container.go
+++ b/internal/ui/messages/container/container.go
@@ -42,7 +42,7 @@ type Container interface {
// CreateMessageUnsafe creates a new message and returns the index that is
// the location the message is added to.
- CreateMessageUnsafe(cchat.MessageCreate)
+ CreateMessageUnsafe(cchat.MessageCreate) MessageRow
UpdateMessageUnsafe(cchat.MessageUpdate)
DeleteMessageUnsafe(cchat.MessageDelete)
@@ -84,9 +84,9 @@ type Controller interface {
// Constructor is an interface for making custom message implementations which
// allows ListContainer to generically work with.
-type Constructor interface {
- NewMessage(cchat.MessageCreate) MessageRow
- NewPresendMessage(input.PresendMessage) PresendMessageRow
+type Constructor struct {
+ NewMessage func(msg cchat.MessageCreate, before MessageRow) MessageRow
+ NewPresendMessage func(msg input.PresendMessage, before MessageRow) PresendMessageRow
}
const ColumnSpacing = 8
@@ -107,10 +107,19 @@ type messageRow struct {
presend message.PresendContainer // this shouldn't be here but i'm lazy
}
+// unwrapRow is a helper that unwraps a messageRow if it's not nil. If it's nil,
+// then a nil interface is returned.
+func unwrapRow(msg *messageRow) MessageRow {
+ if msg == nil || msg.MessageRow == nil {
+ return nil
+ }
+ return msg.MessageRow
+}
+
var _ Container = (*ListContainer)(nil)
-func NewListContainer(constr Constructor, ctrl Controller) *ListContainer {
- listStore := NewListStore(constr, ctrl)
+func NewListContainer(ctrl Controller, constr Constructor) *ListContainer {
+ listStore := NewListStore(ctrl, constr)
listStore.ListBox.Show()
clamp := handy.ClampNew()
@@ -128,11 +137,12 @@ func NewListContainer(constr Constructor, ctrl Controller) *ListContainer {
}
}
-// CreateMessageUnsafe inserts a message. It does not clean up old messages.
-func (c *ListContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
- // Insert the message first.
- c.ListStore.CreateMessageUnsafe(msg)
-}
+// TODO: remove useless abstraction (this file).
+
+// // CreateMessageUnsafe inserts a message. It does not clean up old messages.
+// func (c *ListContainer) CreateMessageUnsafe(msg cchat.MessageCreate) MessageRow {
+// return c.ListStore.CreateMessageUnsafe(msg)
+// }
// CleanMessages cleans up the oldest messages if the user is scrolled to the
// bottom. True is returned if there were changes.
diff --git a/internal/ui/messages/container/cozy/cozy.go b/internal/ui/messages/container/cozy/cozy.go
index e4aa6d0..92c1f6a 100644
--- a/internal/ui/messages/container/cozy/cozy.go
+++ b/internal/ui/messages/container/cozy/cozy.go
@@ -41,61 +41,45 @@ const (
AvatarMargin = 10
)
+var messageConstructors = container.Constructor{
+ NewMessage: NewMessage,
+ NewPresendMessage: NewPresendMessage,
+}
+
+func NewMessage(
+ msg cchat.MessageCreate, before container.MessageRow) container.MessageRow {
+
+ if gridMessageIsAuthor(before, msg.Author()) {
+ return NewCollapsedMessage(msg)
+ }
+
+ return NewFullMessage(msg)
+}
+
+func NewPresendMessage(
+ msg input.PresendMessage, before container.MessageRow) container.PresendMessageRow {
+
+ if gridMessageIsAuthor(before, msg.Author()) {
+ return NewCollapsedSendingMessage(msg)
+ }
+
+ return NewFullSendingMessage(msg)
+}
+
type Container struct {
*container.ListContainer
}
func NewContainer(ctrl container.Controller) *Container {
- c := &Container{}
- c.ListContainer = container.NewListContainer(c, ctrl)
-
+ c := container.NewListContainer(ctrl, messageConstructors)
primitives.AddClass(c, "cozy-container")
- return c
-}
-
-func (c *Container) NewMessage(msg cchat.MessageCreate) container.MessageRow {
- // We're not checking for a collapsed message here anymore, as the
- // CreateMessage method will do that.
-
- // // Is the latest message of the same author? If yes, display it as a
- // // collapsed message.
- // if c.lastMessageIsAuthor(msg.Author().ID()) {
- // return NewCollapsedMessage(msg)
- // }
-
- full := NewFullMessage(msg)
- author := msg.Author()
-
- // Try and reuse an existing avatar if the author has one.
- if avatarURL := author.Avatar(); avatarURL != "" {
- // Try reusing the avatar, but fetch it from the interndet if we can't
- // reuse. The reuse function does this for us.
- c.reuseAvatar(author.ID(), author.Avatar(), full)
- }
-
- return full
-}
-
-func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendMessageRow {
- // We can do the check here since we're never using NewPresendMessage for
- // backlog messages.
- if c.lastMessageIsAuthor(msg.AuthorID(), msg.Author().String(), 0) {
- return NewCollapsedSendingMessage(msg)
- }
-
- full := NewFullSendingMessage(msg)
-
- // Try and see if we can reuse the avatar, and fallback if possible. The
- // avatar URL passed in here will always yield an equal.
- c.reuseAvatar(msg.AuthorID(), msg.AuthorAvatarURL(), &full.FullMessage)
-
- return full
+ 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.AuthorID() == authorID
+ return msgc.Author().ID() == authorID
})
}
@@ -108,32 +92,48 @@ func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) {
// Borrow the avatar pixbuf, but only if the avatar URL is the same.
p, ok := lastAuthorMsg.(AvatarPixbufCopier)
- if ok && lastAuthorMsg.AvatarURL() == avatarURL {
- p.CopyAvatarPixbuf(full.Avatar.Image)
- full.Avatar.ManuallySetURL(avatarURL)
- } else {
- // We can't borrow, so we need to fetch it anew.
- full.Avatar.SetURL(avatarURL)
+ if ok && lastAuthorMsg.Author().Avatar() == avatarURL {
+ if p.CopyAvatarPixbuf(full.Avatar.Image) {
+ full.Avatar.ManuallySetURL(avatarURL)
+ return
+ }
}
+
+ // We can't borrow, so we need to fetch it anew.
+ full.Avatar.SetURL(avatarURL)
}
-func (c *Container) lastMessageIsAuthor(id cchat.ID, name string, offset int) bool {
- // Get the offfsetth message from last.
- var last = c.ListStore.NthMessage((c.ListStore.MessagesLen() - 1) + offset)
- return gridMessageIsAuthor(last, id, name)
-}
+// lastMessageIsAuthor removed - assuming index before insertion is harmful.
-func gridMessageIsAuthor(gridMsg container.MessageRow, id cchat.ID, name string) bool {
- return gridMsg != nil &&
- gridMsg.AuthorID() == id &&
- gridMsg.AuthorName() == name
+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()
}
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
// Create the message in the parent's handler. This handler will also
// wipe old messages.
- c.ListContainer.CreateMessageUnsafe(msg)
+ 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)
+ }
+ }
// Did the handler wipe old messages? It will only do so if the user is
// scrolled to the bottom.
@@ -143,24 +143,12 @@ func (c *Container) CreateMessage(msg cchat.MessageCreate) {
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.ListContainer.LastMessage().ID():
- author := msg.Author()
- if c.lastMessageIsAuthor(author.ID(), author.Name().String(), -1) {
- c.compact(c.ListContainer.LastMessage())
- }
-
// If we've prepended the message, then see if we need to collapse the
// second message.
- case c.ListContainer.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)
- }
+ 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)
}
}
})
@@ -174,15 +162,17 @@ func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
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(msg.ID())
+ 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(msg.ID())
+ msg := c.ListStore.PopMessage(msgID)
// Don't calculate if we don't have any messages, or no messages before
// and after.
@@ -190,8 +180,10 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
return
}
+ msgAuthorID := msg.Author().ID()
+
// Check if the last message is the author's (relative to i):
- if prev.AuthorID() == msg.AuthorID() {
+ if prev.Author().ID() == msgAuthorID {
// If the author is the same, then we don't need to uncollapse the
// message.
return
@@ -199,7 +191,7 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
// If the next message (relative to i) is not the deleted message's
// author, then we don't need to uncollapse it.
- if next.AuthorID() != msg.AuthorID() {
+ if next.Author().ID() != msgAuthorID {
return
}
@@ -211,39 +203,30 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
func (c *Container) uncompact(msg container.MessageRow) {
// We should only uncompact the message if it's compacted in the first
// place.
- if collapse, ok := msg.(Collapsible); !ok || !collapse.Collapsed() {
- return
- }
-
- // We can't unwrap if the message doesn't implement Unwrapper.
- uw, ok := msg.(Unwrapper)
+ compact, ok := msg.(*CollapsedMessage)
if !ok {
return
}
// Start the "lengthy" uncollapse process.
- full := WrapFullMessage(uw.Unwrap())
+ full := WrapFullMessage(compact.Unwrap())
// 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.
- c.reuseAvatar(msg.AuthorID(), msg.AvatarURL(), full)
+ author := msg.Author()
+ c.reuseAvatar(author.ID(), author.Avatar(), full)
// Swap the old next message out for a new one.
c.ListStore.SwapMessage(full)
}
func (c *Container) compact(msg container.MessageRow) {
- // Exit if the message is already collapsed.
- if collapse, ok := msg.(Collapsible); !ok || collapse.Collapsed() {
- return
- }
-
- uw, ok := msg.(Unwrapper)
+ full, ok := msg.(*FullMessage)
if !ok {
return
}
- compact := WrapCollapsedMessage(uw.Unwrap())
+ compact := WrapCollapsedMessage(full.Unwrap())
message.RefreshContainer(compact, compact.GenericContainer)
c.ListStore.SwapMessage(compact)
diff --git a/internal/ui/messages/container/cozy/message_collapsed.go b/internal/ui/messages/container/cozy/message_collapsed.go
index 2a4902f..7d97f66 100644
--- a/internal/ui/messages/container/cozy/message_collapsed.go
+++ b/internal/ui/messages/container/cozy/message_collapsed.go
@@ -16,6 +16,7 @@ import (
type CollapsedMessage struct {
// Author is still updated normally.
*message.GenericContainer
+ Timestamp *gtk.Label
}
func NewCollapsedMessage(msg cchat.MessageCreate) *CollapsedMessage {
@@ -26,21 +27,23 @@ func NewCollapsedMessage(msg cchat.MessageCreate) *CollapsedMessage {
func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
// Set Timestamp's padding accordingly to Avatar's.
- gc.Timestamp.SetSizeRequest(AvatarSize, -1)
- gc.Timestamp.SetVAlign(gtk.ALIGN_START)
- gc.Timestamp.SetXAlign(0.5) // middle align
- gc.Timestamp.SetMarginEnd(container.ColumnSpacing)
- gc.Timestamp.SetMarginStart(container.ColumnSpacing * 2)
+ ts := message.NewTimestamp()
+ ts.SetSizeRequest(AvatarSize, -1)
+ ts.SetVAlign(gtk.ALIGN_START)
+ ts.SetXAlign(0.5) // middle align
+ ts.SetMarginEnd(container.ColumnSpacing)
+ ts.SetMarginStart(container.ColumnSpacing * 2)
// Set Content's padding accordingly to FullMessage's main box.
gc.Content.ToWidget().SetMarginEnd(container.ColumnSpacing * 2)
- gc.PackStart(gc.Timestamp, false, false, 0)
+ gc.PackStart(ts, false, false, 0)
gc.PackStart(gc.Content, true, true, 0)
gc.SetClass("cozy-collapsed")
return &CollapsedMessage{
GenericContainer: gc,
+ Timestamp: ts,
}
}
diff --git a/internal/ui/messages/container/cozy/message_full.go b/internal/ui/messages/container/cozy/message_full.go
index b529931..94de10f 100644
--- a/internal/ui/messages/container/cozy/message_full.go
+++ b/internal/ui/messages/container/cozy/message_full.go
@@ -13,6 +13,8 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
+ "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
+ "github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/cairo"
"github.com/gotk3/gotk3/gtk"
)
@@ -27,12 +29,12 @@ type FullMessage struct {
Avatar *Avatar
MainBox *gtk.Box // wraps header and content
- // Header wraps author and timestamp.
- HeaderBox *gtk.Box
+ Header *labeluri.Label
+ timestamp string // markup
}
type AvatarPixbufCopier interface {
- CopyAvatarPixbuf(img httputil.SurfaceContainer)
+ CopyAvatarPixbuf(img httputil.SurfaceContainer) bool
}
var (
@@ -41,10 +43,6 @@ var (
_ container.MessageRow = (*FullMessage)(nil)
)
-var boldCSS = primitives.PrepareCSS(`
- * { font-weight: 600; }
-`)
-
var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", `
/* Slightly dip down on click */
.cozy-avatar:active {
@@ -63,33 +61,26 @@ func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
}
func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
+ header := labeluri.NewLabel(text.Rich{})
+ header.SetHAlign(gtk.ALIGN_START) // left-align
+ header.SetMaxWidthChars(100)
+ header.Show()
+
avatar := NewAvatar()
avatar.SetMarginTop(TopFullMargin / 2)
avatar.SetMarginStart(container.ColumnSpacing * 2)
avatar.Connect("clicked", func(w gtk.IWidget) {
- if output := gc.Username.Output(); len(output.Mentions) > 0 {
+ if output := header.Output(); len(output.Mentions) > 0 {
labeluri.PopoverMentioner(w, output.Input, output.Mentions[0])
}
})
avatar.Show()
- // Style the timestamp accordingly.
- gc.Timestamp.SetXAlign(0.0) // left-align
- 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)
// Attach the username style provider.
- primitives.AttachCSS(gc.Username, boldCSS)
-
- header, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
- header.PackStart(gc.Username, false, false, 0)
- header.PackStart(gc.Timestamp, false, false, 7) // padding
- header.Show()
+ // primitives.AttachCSS(gc.Username, boldCSS)
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
main.PackStart(header, false, false, 0)
@@ -108,9 +99,10 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
return &FullMessage{
GenericContainer: gc,
- Avatar: avatar,
- MainBox: main,
- HeaderBox: header,
+
+ Avatar: avatar,
+ MainBox: main,
+ Header: header,
}
}
@@ -118,17 +110,12 @@ func (m *FullMessage) Collapsed() bool { return false }
func (m *FullMessage) Unwrap() *message.GenericContainer {
// Remove GenericContainer's widgets from the containers.
- m.HeaderBox.Remove(m.Username)
- m.HeaderBox.Remove(m.Timestamp)
- m.MainBox.Remove(m.HeaderBox)
- m.MainBox.Remove(m.Content)
-
- // Hide the avatar.
- m.Avatar.Hide()
+ m.Header.Destroy()
+ m.MainBox.Remove(m.Content) // not ours, so don't destroy.
// Remove the message from the grid.
- m.Remove(m.Avatar)
- m.Remove(m.MainBox)
+ m.Avatar.Destroy()
+ m.MainBox.Destroy()
// Return after removing.
return m.GenericContainer
@@ -136,18 +123,36 @@ func (m *FullMessage) Unwrap() *message.GenericContainer {
func (m *FullMessage) UpdateTimestamp(t time.Time) {
m.GenericContainer.UpdateTimestamp(t)
- m.Timestamp.SetText(humanize.TimeAgoLong(t))
+
+ m.timestamp = " " +
+ `` + humanize.TimeAgoLong(t) + ``
+
+ // Update the timestamp.
+ m.Header.SetMarkup(m.Header.Output().Markup + m.timestamp)
}
func (m *FullMessage) UpdateAuthor(author cchat.Author) {
- // Call the parent's method to update the labels.
+ // Call the parent's method to update the state.
m.GenericContainer.UpdateAuthor(author)
+ m.UpdateAuthorName(author.Name())
m.Avatar.SetURL(author.Avatar())
}
+func (m *FullMessage) UpdateAuthorName(name text.Rich) {
+ cfg := markup.RenderConfig{}
+ cfg.NoReferencing = true
+ cfg.SetForegroundAnchor(m.ContentBodyStyle)
+
+ output := markup.RenderCmplxWithConfig(name, cfg)
+ output.Markup = `` + output.Markup + ""
+
+ m.Header.SetMarkup(output.Markup + m.timestamp)
+ m.Header.SetUnderlyingOutput(output)
+}
+
// CopyAvatarPixbuf sets the pixbuf into the given container. This shares the
// same pixbuf, but gtk.Image should take its own reference from the pixbuf.
-func (m *FullMessage) CopyAvatarPixbuf(dst httputil.SurfaceContainer) {
+func (m *FullMessage) CopyAvatarPixbuf(dst httputil.SurfaceContainer) bool {
switch img := m.Avatar.Image.GetImage(); img.GetStorageType() {
case gtk.IMAGE_PIXBUF:
dst.SetFromPixbuf(img.GetPixbuf())
@@ -156,7 +161,10 @@ func (m *FullMessage) CopyAvatarPixbuf(dst httputil.SurfaceContainer) {
case gtk.IMAGE_SURFACE:
v, _ := img.GetProperty("surface")
dst.SetFromSurface(v.(*cairo.Surface))
+ default:
+ return false
}
+ return true
}
func (m *FullMessage) AttachMenu(items []menu.Item) {
diff --git a/internal/ui/messages/container/list.go b/internal/ui/messages/container/list.go
index fa80474..9746fef 100644
--- a/internal/ui/messages/container/list.go
+++ b/internal/ui/messages/container/list.go
@@ -1,7 +1,6 @@
package container
import (
- "log"
"time"
"github.com/diamondburned/cchat"
@@ -35,7 +34,7 @@ type ListStore struct {
messages map[messageKey]*messageRow
}
-func NewListStore(constr Constructor, ctrl Controller) *ListStore {
+func NewListStore(ctrl Controller, constr Constructor) *ListStore {
listBox, _ := gtk.ListBoxNew()
listBox.SetSelectionMode(gtk.SELECTION_SINGLE)
listBox.Show()
@@ -154,12 +153,9 @@ func (c *ListStore) around(aroundID cchat.ID) (before, after *messageRow) {
// LatestMessageFrom returns the latest message with the given user ID. This is
// used for the input prompt.
func (c *ListStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
- log.Println("LatestMessageFrom called")
-
// FindMessage already looks from the latest messages.
var msg = c.FindMessage(func(msg MessageRow) bool {
- log.Println("Author:", msg.AuthorName())
- return msg.AuthorID() == userID
+ return msg.Author().ID() == userID
})
if msg == nil {
@@ -229,25 +225,22 @@ func (c *ListStore) FindMessage(isMessage func(MessageRow) bool) MessageRow {
msg, _ := c.findMessage(false, func(row *messageRow) bool {
return isMessage(row.MessageRow)
})
- if msg != nil {
- return msg.MessageRow
- }
- return nil
+ return unwrapRow(msg)
}
func (c *ListStore) nthMessage(n int) *messageRow {
v := primitives.NthChild(c.ListBox, n)
+ if v == nil {
+ return nil
+ }
+
id := primitives.GetName(v.(primitives.Namer))
return c.message(id, "")
}
// NthMessage returns the nth message.
func (c *ListStore) NthMessage(n int) MessageRow {
- msg := c.nthMessage(n)
- if msg != nil {
- return msg.MessageRow
- }
- return nil
+ return unwrapRow(c.nthMessage(n))
}
// FirstMessage returns the first message.
@@ -263,10 +256,7 @@ func (c *ListStore) LastMessage() MessageRow {
// Message finds the message state in the container. It is not thread-safe. This
// exists for backwards compatibility.
func (c *ListStore) Message(msgID cchat.ID, nonce string) MessageRow {
- if m := c.message(msgID, nonce); m != nil {
- return m.MessageRow
- }
- return nil
+ return unwrapRow(c.message(msgID, nonce))
}
func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
@@ -300,7 +290,8 @@ func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
// AddPresendMessage inserts an input.PresendMessage into the container and
// returning a wrapped widget interface.
func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRow {
- presend := c.Construct.NewPresendMessage(msg)
+ before := c.LastMessage()
+ presend := c.Construct.NewPresendMessage(msg, before)
msgc := &messageRow{
MessageRow: presend,
@@ -326,7 +317,7 @@ func (c *ListStore) bindMessage(msgc *messageRow) {
// unreliable. The index might be off if the message buffer is cleaned up. Don't
// rely on it.
-func (c *ListStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
+func (c *ListStore) CreateMessageUnsafe(msg cchat.MessageCreate) MessageRow {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
@@ -337,33 +328,45 @@ func (c *ListStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
msgc.UpdateTimestamp(msg.Time())
c.bindMessage(msgc)
- return
+ return msgc.MessageRow
}
- msgc := &messageRow{
- MessageRow: c.Construct.NewMessage(msg),
- }
msgTime := msg.Time()
- // Iterate and compare timestamp to find where to insert a message.
- after, index := c.findMessage(true, func(after *messageRow) bool {
- return msgTime.After(after.Time())
+ // Iterate and compare timestamp to find where to insert a message. Note
+ // that "before" is the message that will go before the to-be-inserted
+ // method.
+ before, index := c.findMessage(true, func(before *messageRow) bool {
+ return msgTime.After(before.Time())
})
- // Append the message. If after is nil, then that means the message is the
- // oldest, so we add it to the front of the list.
- if after != nil {
- index++ // insert right after
- c.ListBox.Insert(msgc.Row(), index)
- } else {
+ msgc := &messageRow{
+ MessageRow: c.Construct.NewMessage(msg, unwrapRow(before)),
+ }
+
+ // Add the message. If before is nil, then the to-be-inserted message is the
+ // earliest message, therefore we prepend it.
+ if before == nil {
index = 0
- c.ListBox.Add(msgc.Row())
+ c.ListBox.Prepend(msgc.Row())
+ } else {
+ index++ // insert right after
+
+ // Fast path: Insert did appear a lot on profiles, so we can try and use
+ // Add over Insert when we know.
+ if c.MessagesLen() == index {
+ c.ListBox.Add(msgc.Row())
+ } else {
+ c.ListBox.Insert(msgc.Row(), index)
+ }
}
// Set the ID into the message map.
c.messages[idKey(msgc.ID())] = msgc
c.bindMessage(msgc)
+
+ return msgc.MessageRow
}
func (c *ListStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
@@ -416,8 +419,6 @@ func (c *ListStore) DeleteEarliest(n int) {
id := primitives.GetName(v.(primitives.Namer))
gridMsg := c.message(id, "")
- log.Println("Deleting overflowed message ID from", gridMsg.AuthorName())
-
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 3753e40..084a016 100644
--- a/internal/ui/messages/input/input.go
+++ b/internal/ui/messages/input/input.go
@@ -11,6 +11,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
+ "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
@@ -20,7 +21,7 @@ import (
type Controller interface {
AddPresendMessage(msg PresendMessage) (onErr func(error))
LatestMessageFrom(userID cchat.ID) (messageID cchat.ID, ok bool)
- MessageAuthorMarkup(msgID cchat.ID) (markup string, ok bool)
+ MessageAuthor(msgID cchat.ID) cchat.Author
}
// LabelBorrower is an interface that allows the caller to borrow a label.
@@ -330,12 +331,22 @@ func (f *Field) StartReplyingTo(msgID cchat.ID) {
f.replyingID = msgID
f.sendIcon.SetFromIconName(replyButtonIcon, gtk.ICON_SIZE_BUTTON)
- name, ok := f.ctrl.MessageAuthorMarkup(msgID)
- if !ok {
- name = "message"
+ if author := f.ctrl.MessageAuthor(msgID); author != nil {
+ // Extract the name from the author's rich text and only render the area
+ // with the MessageReference.
+ name := author.Name()
+
+ for _, seg := range name.Segments {
+ if seg.AsMessageReferencer() != nil || seg.AsMentioner() != nil {
+ mention := markup.Render(markup.SubstringSegment(name, seg))
+ f.indicator.BorrowLabel("Replying to " + mention)
+ return
+ }
+ }
}
- f.indicator.BorrowLabel("Replying to " + name)
+ f.indicator.BorrowLabel("Replying to message.")
+ return
}
// Editable returns whether or not the input field can be edited.
diff --git a/internal/ui/messages/input/sendable.go b/internal/ui/messages/input/sendable.go
index d0cb93c..469e8eb 100644
--- a/internal/ui/messages/input/sendable.go
+++ b/internal/ui/messages/input/sendable.go
@@ -64,14 +64,12 @@ func (f *Field) sendInput() {
}
f.SendMessage(SendMessageData{
- time: time.Now().UTC(),
- content: text,
- author: f.Username.GetLabel(),
- authorID: f.UserID,
- authorURL: f.Username.GetIconURL(),
- nonce: f.generateNonce(),
- replyID: f.replyingID,
- files: attachments,
+ time: time.Now().UTC(),
+ content: text,
+ author: newAuthor(f),
+ nonce: f.generateNonce(),
+ replyID: f.replyingID,
+ files: attachments,
})
// Clear the input field after sending.
@@ -110,14 +108,12 @@ func (files Files) Attachments() []cchat.MessageAttachment {
// SendMessageData contains what is to be sent in a message. It behaves
// similarly to a regular CreateMessage.
type SendMessageData struct {
- time time.Time
- content string
- author text.Rich
- authorID cchat.ID
- authorURL string // avatar
- nonce string
- replyID cchat.ID
- files Files
+ time time.Time
+ content string
+ author cchat.Author
+ nonce string
+ replyID cchat.ID
+ files Files
}
var _ cchat.SendableMessage = (*SendMessageData)(nil)
@@ -130,9 +126,7 @@ type PresendMessage interface {
// These methods are reserved for internal use.
- Author() text.Rich
- AuthorID() string
- AuthorAvatarURL() string // may be empty
+ Author() cchat.Author
Files() []attachment.File
}
@@ -142,12 +136,30 @@ var _ PresendMessage = (*SendMessageData)(nil)
func (s SendMessageData) ID() string { return s.nonce }
func (s SendMessageData) Time() time.Time { return s.time }
func (s SendMessageData) Content() string { return s.content }
-func (s SendMessageData) Author() text.Rich { return s.author }
-func (s SendMessageData) AuthorID() string { return s.authorID }
-func (s SendMessageData) AuthorAvatarURL() string { return s.authorURL }
+func (s SendMessageData) Author() cchat.Author { return s.author }
func (s SendMessageData) AsNoncer() cchat.Noncer { return s }
func (s SendMessageData) Nonce() string { return s.nonce }
func (s SendMessageData) Files() []attachment.File { return s.files }
func (s SendMessageData) AsAttacher() cchat.Attacher { return s.files }
func (s SendMessageData) AsReplier() cchat.Replier { return s }
func (s SendMessageData) ReplyingTo() cchat.ID { return s.replyID }
+
+type sendableAuthor struct {
+ id cchat.ID
+ name text.Rich
+ avatarURL string
+}
+
+func newAuthor(f *Field) sendableAuthor {
+ return sendableAuthor{
+ f.UserID,
+ f.Username.GetLabel(),
+ f.Username.GetIconURL(),
+ }
+}
+
+var _ cchat.Author = (*sendableAuthor)(nil)
+
+func (a sendableAuthor) ID() string { return a.id }
+func (a sendableAuthor) Name() text.Rich { return a.name }
+func (a sendableAuthor) Avatar() string { return a.avatarURL }
diff --git a/internal/ui/messages/memberlist/eventqueue.go b/internal/ui/messages/memberlist/eventqueue.go
index 649d30f..61aa48e 100644
--- a/internal/ui/messages/memberlist/eventqueue.go
+++ b/internal/ui/messages/memberlist/eventqueue.go
@@ -29,7 +29,7 @@ func (evq *eventQueue) Add(fn func()) {
if evq.activated {
evq.idleQueue = append(evq.idleQueue, fn)
} else {
- gts.ExecAsync(fn)
+ gts.ExecLater(fn)
}
}
@@ -54,18 +54,25 @@ func (evq *eventQueue) pop() []func() {
func (evq *eventQueue) Deactivate() {
var popped = evq.pop()
+ const chunkSz = 25
+
// We shouldn't try and run more than a certain amount of callbacks within a
// single loop, as it will freeze up the UI.
- if len(popped) > 25 {
- for _, fn := range popped {
- gts.ExecAsync(fn)
- }
- return
- }
+ for i := 0; i < len(popped); i += chunkSz {
+ // Calculate the bounds in chunks.
+ start, end := i, min(i+chunkSz, len(popped))
- gts.ExecAsync(func() {
- for _, fn := range popped {
- fn()
- }
- })
+ gts.ExecLater(func() {
+ for _, fn := range popped[start:end] {
+ fn()
+ }
+ })
+ }
+}
+
+func min(i, j int) int {
+ if i < j {
+ return i
+ }
+ return j
}
diff --git a/internal/ui/messages/memberlist/memberlist.go b/internal/ui/messages/memberlist/memberlist.go
index d0e9188..363c0b4 100644
--- a/internal/ui/messages/memberlist/memberlist.go
+++ b/internal/ui/messages/memberlist/memberlist.go
@@ -366,6 +366,7 @@ func NewMember(member cchat.ListMember) *Member {
var noMentionLinks = markup.RenderConfig{
NoMentionLinks: true,
+ NoReferencing: true,
}
func (m *Member) Update(member cchat.ListMember) {
@@ -376,9 +377,10 @@ func (m *Member) Update(member cchat.ListMember) {
}
m.output = markup.RenderCmplxWithConfig(member.Name(), noMentionLinks)
+
txt := strings.Builder{}
txt.WriteString(fmt.Sprintf(
- `● %s`,
+ `● %s`,
statusColors(member.Status()), m.output.Markup,
))
@@ -395,20 +397,22 @@ func (m *Member) Update(member cchat.ListMember) {
// Popup pops up the mention popover if any.
func (m *Member) Popup(evq EventQueuer) {
- if len(m.output.Mentions) > 0 {
- p := labeluri.NewPopoverMentioner(m, m.output.Input, m.output.Mentions[0])
- if p == nil {
- return
- }
-
- // Unbounded concurrency is kind of bad. We should deal with
- // this in the future.
- evq.Activate()
- p.Connect("closed", func(interface{}) { evq.Deactivate() })
-
- p.SetPosition(gtk.POS_LEFT)
- p.Popup()
+ if len(m.output.Mentions) == 0 {
+ return
}
+
+ p := labeluri.NewPopoverMentioner(m, m.output.Input, m.output.Mentions[0])
+ if p == nil {
+ return
+ }
+
+ // Unbounded concurrency is kind of bad. We should deal with
+ // this in the future.
+ evq.Activate()
+ p.Connect("closed", func(interface{}) { evq.Deactivate() })
+
+ p.SetPosition(gtk.POS_LEFT)
+ p.Popup()
}
func statusColors(status cchat.Status) uint32 {
diff --git a/internal/ui/messages/message/author.go b/internal/ui/messages/message/author.go
new file mode 100644
index 0000000..88e0daa
--- /dev/null
+++ b/internal/ui/messages/message/author.go
@@ -0,0 +1,50 @@
+package message
+
+import (
+ "github.com/diamondburned/cchat"
+ "github.com/diamondburned/cchat/text"
+)
+
+// Author implements cchat.Author. It effectively contains a copy of
+// cchat.Author.
+type Author struct {
+ id cchat.ID
+ name text.Rich
+ avatarURL string
+}
+
+var _ cchat.Author = (*Author)(nil)
+
+// NewAuthor creates a new Author that is a copy of the given author.
+func NewAuthor(author cchat.Author) Author {
+ a := Author{}
+ a.Update(author)
+ return a
+}
+
+// NewCustomAuthor creates a new Author from the given parameters.
+func NewCustomAuthor(id cchat.ID, name text.Rich, avatar string) Author {
+ return Author{
+ id,
+ name,
+ avatar,
+ }
+}
+
+func (a *Author) Update(author cchat.Author) {
+ a.id = author.ID()
+ a.name = author.Name()
+ a.avatarURL = author.Avatar()
+}
+
+func (a Author) ID() string {
+ return a.id
+}
+
+func (a Author) Name() text.Rich {
+ return a.name
+}
+
+func (a Author) Avatar() string {
+ return a.avatarURL
+}
diff --git a/internal/ui/messages/message/message.go b/internal/ui/messages/message/message.go
index 1b2ce8d..5318ce0 100644
--- a/internal/ui/messages/message/message.go
+++ b/internal/ui/messages/message/message.go
@@ -4,28 +4,22 @@ import (
"time"
"github.com/diamondburned/cchat"
- "github.com/diamondburned/cchat-gtk/internal/humanize"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
- "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
type Container interface {
- ID() string
+ ID() cchat.ID
Time() time.Time
- AuthorID() string
- AuthorName() string
- AuthorMarkup() string
- AvatarURL() string // avatar
+ Author() cchat.Author
Nonce() string
UpdateAuthor(cchat.Author)
- UpdateAuthorName(text.Rich)
UpdateContent(c text.Rich, edited bool)
UpdateTimestamp(time.Time)
}
@@ -40,8 +34,6 @@ func FillContainer(c Container, msg cchat.MessageCreate) {
// RefreshContainer sets the container's contents to the one from
// GenericContainer. This is mainly used for transferring between different
// containers.
-//
-// Right now, this only works with Timestamp, as that's the only state tracked.
func RefreshContainer(c Container, gc *GenericContainer) {
c.UpdateTimestamp(gc.time)
}
@@ -53,40 +45,20 @@ type GenericContainer struct {
row *gtk.ListBoxRow // contains Box
class string
- id string
- time time.Time
- authorID string
- authorName string
- avatarURL string // avatar
- nonce string
+ id string
+ time time.Time
+ author Author
+ nonce string
- Timestamp *gtk.Label
- Username *labeluri.Label
- Content gtk.IWidget // conceal widget implementation
-
- contentBox *gtk.Box // basically what is in Content
- ContentBody *labeluri.Label
+ Content *gtk.Box
+ ContentBody *labeluri.Label
+ ContentBodyStyle *gtk.StyleContext
menuItems []menu.Item
}
var _ Container = (*GenericContainer)(nil)
-var timestampCSS = primitives.PrepareClassCSS("message-time", `
- .message-time {
- opacity: 0.3;
- font-size: 0.8em;
- margin-top: 0.2em;
- margin-bottom: 0.2em;
- }
-`)
-
-var authorCSS = primitives.PrepareClassCSS("message-author", `
- .message-author {
- color: mix(@theme_bg_color, @theme_fg_color, 0.8);
- }
-`)
-
// NewContainer creates a new message container with the given ID and nonce. It
// does not update the widgets, so FillContainer should be called afterwards.
func NewContainer(msg cchat.MessageCreate) *GenericContainer {
@@ -94,24 +66,12 @@ func NewContainer(msg cchat.MessageCreate) *GenericContainer {
c.id = msg.ID()
c.time = msg.Time()
c.nonce = msg.Nonce()
- c.authorID = msg.Author().ID()
+ c.author.Update(msg.Author())
return c
}
func NewEmptyContainer() *GenericContainer {
- ts, _ := gtk.LabelNew("")
- ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
- ts.SetXAlign(0.5) // centre align
- ts.SetVAlign(gtk.ALIGN_END)
- ts.Show()
-
- user := labeluri.NewLabel(text.Rich{})
- user.SetXAlign(0) // left align
- user.SetVAlign(gtk.ALIGN_START)
- user.SetTrackVisitedLinks(false)
- user.Show()
-
ctbody := labeluri.NewLabel(text.Rich{})
ctbody.SetVExpand(true)
ctbody.SetHAlign(gtk.ALIGN_START)
@@ -123,6 +83,9 @@ func NewEmptyContainer() *GenericContainer {
ctbody.SetTrackVisitedLinks(false)
ctbody.Show()
+ ctbodyStyle, _ := ctbody.GetStyleContext()
+ ctbodyStyle.AddClass("message-content")
+
// Wrap the content label inside a content box.
ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
ctbox.SetHExpand(true)
@@ -135,24 +98,15 @@ func NewEmptyContainer() *GenericContainer {
row, _ := gtk.ListBoxRowNew()
row.Add(box)
row.Show()
-
- // Add CSS classes.
- primitives.AddClass(ts, "message-time")
primitives.AddClass(row, "message-row")
- primitives.AddClass(user, "message-author")
- primitives.AddClass(ctbody, "message-content")
- timestampCSS(ts)
- authorCSS(ts)
gc := &GenericContainer{
Box: box,
row: row,
- Timestamp: ts,
- Username: user,
- Content: ctbox,
- contentBox: ctbox,
- ContentBody: ctbody,
+ Content: ctbox,
+ ContentBody: ctbody,
+ ContentBodyStyle: ctbodyStyle,
// Time is important, as it is used to sort messages, so we have to be
// careful with this.
@@ -183,7 +137,6 @@ func (m *GenericContainer) SetClass(class string) {
// SetReferenceHighlighter sets the reference highlighter into the message.
func (m *GenericContainer) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
- m.Username.SetReferenceHighlighter(r)
m.ContentBody.SetReferenceHighlighter(r)
}
@@ -195,20 +148,8 @@ func (m *GenericContainer) Time() time.Time {
return m.time
}
-func (m *GenericContainer) AuthorID() string {
- return m.authorID
-}
-
-func (m *GenericContainer) AuthorName() string {
- return m.authorName
-}
-
-func (m *GenericContainer) AuthorMarkup() string {
- return m.Username.Label.Label.GetLabel()
-}
-
-func (m *GenericContainer) AvatarURL() string {
- return m.avatarURL
+func (m *GenericContainer) Author() cchat.Author {
+ return m.author
}
func (m *GenericContainer) Nonce() string {
@@ -217,23 +158,10 @@ func (m *GenericContainer) Nonce() string {
func (m *GenericContainer) UpdateTimestamp(t time.Time) {
m.time = t
- m.Timestamp.SetText(humanize.TimeAgo(t))
- m.Timestamp.SetTooltipText(t.Format(time.Stamp))
}
func (m *GenericContainer) UpdateAuthor(author cchat.Author) {
- m.authorID = author.ID()
- m.avatarURL = author.Avatar()
- m.UpdateAuthorName(author.Name())
-}
-
-func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
- cfg := markup.RenderConfig{}
- cfg.NoReferencing = true
- cfg.SetForegroundAnchor(m.ContentBody)
-
- m.authorName = name.String()
- m.Username.SetOutput(markup.RenderCmplxWithConfig(name, cfg))
+ m.author.Update(author)
}
func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
diff --git a/internal/ui/messages/message/sending.go b/internal/ui/messages/message/sending.go
index af7c448..c79b550 100644
--- a/internal/ui/messages/message/sending.go
+++ b/internal/ui/messages/message/sending.go
@@ -35,14 +35,10 @@ type GenericPresendContainer struct {
var _ PresendContainer = (*GenericPresendContainer)(nil)
func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
- return WrapPresendContainer(NewEmptyContainer(), msg)
-}
-
-func WrapPresendContainer(c *GenericContainer, msg input.PresendMessage) *GenericPresendContainer {
+ c := NewEmptyContainer()
c.nonce = msg.Nonce()
- c.authorID = msg.AuthorID()
+ c.UpdateAuthor(msg.Author())
c.UpdateTimestamp(msg.Time())
- c.UpdateAuthorName(msg.Author())
p := &GenericPresendContainer{
GenericContainer: c,
@@ -56,7 +52,7 @@ func WrapPresendContainer(c *GenericContainer, msg input.PresendMessage) *Generi
}
func (m *GenericPresendContainer) SetSensitive(sensitive bool) {
- m.contentBox.SetSensitive(sensitive)
+ m.Content.SetSensitive(sensitive)
}
func (m *GenericPresendContainer) SetDone(id string) {
@@ -68,13 +64,13 @@ func (m *GenericPresendContainer) SetDone(id string) {
// free it from memory.
m.presend = nil
m.uploads = nil
- m.contentBox.SetTooltipText("")
+ m.Content.SetTooltipText("")
// Remove everything in the content box.
m.clearBox()
// Re-add the content label.
- m.contentBox.Add(m.ContentBody)
+ m.Content.Add(m.ContentBody)
// Set the sensitivity from false in SetLoading back to true.
m.SetSensitive(true)
@@ -82,18 +78,18 @@ func (m *GenericPresendContainer) SetDone(id string) {
func (m *GenericPresendContainer) SetLoading() {
m.SetSensitive(false)
- m.contentBox.SetTooltipText("")
+ m.Content.SetTooltipText("")
// Clear everything inside the content container.
m.clearBox()
// Add the content label.
- m.contentBox.Add(m.ContentBody)
+ m.Content.Add(m.ContentBody)
// Add the attachment progress box back in, if any.
if m.uploads != nil {
m.uploads.Show() // show the bars
- m.contentBox.Add(m.uploads)
+ m.Content.Add(m.uploads)
}
if content := m.presend.Content(); content != "" {
@@ -106,13 +102,13 @@ func (m *GenericPresendContainer) SetLoading() {
func (m *GenericPresendContainer) SetSentError(err error) {
m.SetSensitive(true) // allow events incl right clicks
- m.contentBox.SetTooltipText(err.Error())
+ m.Content.SetTooltipText(err.Error())
// Remove everything again.
m.clearBox()
// Re-add the label.
- m.contentBox.Add(m.ContentBody)
+ m.Content.Add(m.ContentBody)
// Style the label appropriately by making it red.
var content = EmptyContentPlaceholder
@@ -132,13 +128,10 @@ func (m *GenericPresendContainer) SetSentError(err error) {
))
errl.Show()
- m.contentBox.Add(errl)
+ m.Content.Add(errl)
}
// clearBox clears everything inside the content container.
func (m *GenericPresendContainer) clearBox() {
- primitives.ForeachChild(m.contentBox, func(v interface{}) (stop bool) {
- m.contentBox.Remove(v.(gtk.IWidget))
- return false
- })
+ primitives.RemoveChildren(m.Content)
}
diff --git a/internal/ui/messages/message/timestamp.go b/internal/ui/messages/message/timestamp.go
new file mode 100644
index 0000000..d70be98
--- /dev/null
+++ b/internal/ui/messages/message/timestamp.go
@@ -0,0 +1,27 @@
+package message
+
+import (
+ "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
+ "github.com/gotk3/gotk3/gtk"
+ "github.com/gotk3/gotk3/pango"
+)
+
+var timestampCSS = primitives.PrepareClassCSS("message-time", `
+ .message-time {
+ opacity: 0.3;
+ font-size: 0.8em;
+ margin-top: 0.2em;
+ margin-bottom: 0.2em;
+ }
+`)
+
+func NewTimestamp() *gtk.Label {
+ ts, _ := gtk.LabelNew("")
+ ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
+ ts.SetXAlign(0.5) // centre align
+ ts.SetVAlign(gtk.ALIGN_END)
+ ts.Show()
+
+ timestampCSS(ts)
+ return ts
+}
diff --git a/internal/ui/messages/message/username.go b/internal/ui/messages/message/username.go
new file mode 100644
index 0000000..a28e154
--- /dev/null
+++ b/internal/ui/messages/message/username.go
@@ -0,0 +1,25 @@
+package message
+
+import (
+ "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
+ "github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
+ "github.com/diamondburned/cchat/text"
+ "github.com/gotk3/gotk3/gtk"
+)
+
+var authorCSS = primitives.PrepareClassCSS("message-author", `
+ .message-author {
+ color: mix(@theme_bg_color, @theme_fg_color, 0.8);
+ }
+`)
+
+func NewUsername() *labeluri.Label {
+ user := labeluri.NewLabel(text.Rich{})
+ user.SetXAlign(0) // left align
+ user.SetVAlign(gtk.ALIGN_START)
+ user.SetTrackVisitedLinks(false)
+ user.Show()
+
+ authorCSS(user)
+ return user
+}
diff --git a/internal/ui/messages/sadface/sadface.go b/internal/ui/messages/sadface/sadface.go
index 450f000..1e251a1 100644
--- a/internal/ui/messages/sadface/sadface.go
+++ b/internal/ui/messages/sadface/sadface.go
@@ -14,8 +14,10 @@ type FaceView struct {
gtk.Stack
placeholder gtk.IWidget
- Face *Container
- Loading *Spinner
+ face *Container
+ loading *Spinner
+ parent gtk.IWidget
+ empty gtk.IWidget
}
func New(parent gtk.IWidget, placeholder gtk.IWidget) *FaceView {
@@ -31,42 +33,50 @@ func New(parent gtk.IWidget, placeholder gtk.IWidget) *FaceView {
stack, _ := gtk.StackNew()
stack.SetTransitionDuration(55)
stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
- stack.AddNamed(parent, "main")
- stack.AddNamed(placeholder, "placeholder")
- stack.AddNamed(c, "face")
- stack.AddNamed(s, "loading")
- stack.AddNamed(b, "empty")
+ stack.Add(parent)
+ stack.Add(c)
+ stack.Add(s)
+ stack.Add(b)
// Show placeholder by default.
- stack.SetVisibleChildName("placeholder")
+ stack.AddNamed(placeholder, "placeholder")
+ stack.SetVisibleChild(placeholder)
- return &FaceView{*stack, placeholder, c, s}
+ return &FaceView{
+ Stack: *stack,
+ placeholder: placeholder,
+
+ face: c,
+ loading: s,
+ parent: parent,
+ empty: b,
+ }
}
// Reset brings the view to an empty box.
func (v *FaceView) Reset() {
- v.Loading.Spinner.Stop()
- v.Stack.SetVisibleChildName("empty")
+ v.loading.Spinner.Stop()
+ v.Stack.SetVisibleChild(v.empty)
v.ensurePlaceholderDestroyed()
}
func (v *FaceView) SetMain() {
- v.Loading.Spinner.Stop()
- v.Stack.SetVisibleChildName("main")
+ v.loading.Spinner.Stop()
+ v.Stack.SetVisibleChild(v.parent)
v.ensurePlaceholderDestroyed()
}
func (v *FaceView) SetLoading() {
- v.Loading.Spinner.Start()
- v.Stack.SetVisibleChildName("loading")
+ v.loading.Spinner.Start()
+ v.Stack.SetVisibleChild(v.loading)
v.ensurePlaceholderDestroyed()
}
func (v *FaceView) SetError(err error) {
- v.Face.SetError(err)
- v.Stack.SetVisibleChildName("face")
+ v.face.SetError(err)
+ v.Stack.SetVisibleChild(v.face)
v.ensurePlaceholderDestroyed()
- v.Loading.Spinner.Stop()
+ v.loading.Spinner.Stop()
}
func (v *FaceView) ensurePlaceholderDestroyed() {
@@ -74,7 +84,7 @@ func (v *FaceView) ensurePlaceholderDestroyed() {
if v.placeholder != nil {
// Safely remove the placeholder from the stack.
if v.Stack.GetVisibleChildName() == "placeholder" {
- v.Stack.SetVisibleChildName("empty")
+ v.Stack.SetVisibleChild(v.empty)
}
// Remove the placeholder widget.
diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go
index dbe6bc6..6ae5f86 100644
--- a/internal/ui/messages/view.go
+++ b/internal/ui/messages/view.go
@@ -232,8 +232,8 @@ func (v *View) Reset() {
// reset resets the message view, but does not change visible containers.
func (v *View) reset() {
- v.Header.Reset() // Reset the header.
v.state.Reset() // Reset the state variables.
+ v.Header.Reset() // Reset the header.
v.Typing.Reset() // Reset the typing state.
v.InputView.Reset() // Reset the input.
v.MemberList.Reset() // Reset the member list.
@@ -397,13 +397,13 @@ func (v *View) AuthorEvent(author cchat.Author) {
}
}
-func (v *View) MessageAuthorMarkup(msgID cchat.ID) (string, bool) {
+func (v *View) MessageAuthor(msgID cchat.ID) cchat.Author {
msg := v.Container.Message(msgID, "")
if msg == nil {
- return "", false
+ return nil
}
- return msg.AuthorMarkup(), true
+ return msg.Author()
}
// LatestMessageFrom returns the last message ID with that author.
diff --git a/internal/ui/primitives/primitives.go b/internal/ui/primitives/primitives.go
index fba9e9d..b687d60 100644
--- a/internal/ui/primitives/primitives.go
+++ b/internal/ui/primitives/primitives.go
@@ -26,13 +26,13 @@ type Container interface {
var _ Container = (*gtk.Container)(nil)
func RemoveChildren(w Container) {
- type destroyer interface {
- Destroy()
- }
+ // type destroyer interface {
+ // Destroy()
+ // }
- children := w.GetChildren()
- children.Foreach(func(child interface{}) { w.Remove(child.(gtk.IWidget)) })
- children.Free()
+ w.GetChildren().FreeFull(func(child interface{}) {
+ w.Remove(child.(gtk.IWidget))
+ })
}
// ChildrenLen gets the total count of children for the given container.
@@ -47,9 +47,15 @@ func NthChild(w Container, n int) interface{} {
children := w.GetChildren()
defer children.Free()
+ // Bound check!
+ if n < 0 || int(children.Length()) >= n {
+ return nil
+ }
+
if n == 0 {
return children.Data()
}
+
return children.NthData(uint(n))
}
@@ -324,7 +330,9 @@ func PrepareClassCSS(class, css string) (attach func(StyleContexter)) {
return func(ctx StyleContexter) {
s, _ := ctx.GetStyleContext()
s.AddProvider(prov, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
- s.AddClass(class)
+ if class != "" {
+ s.AddClass(class)
+ }
}
}
diff --git a/internal/ui/rich/labeluri/labeluri.go b/internal/ui/rich/labeluri/labeluri.go
index 0cd9130..7365655 100644
--- a/internal/ui/rich/labeluri/labeluri.go
+++ b/internal/ui/rich/labeluri/labeluri.go
@@ -82,12 +82,20 @@ func (l *Label) Output() markup.RenderOutput {
return l.output
}
-// SetOutput sets the internal output and label.
+// SetOutput sets the internal output and label. It preserves the tail if
+// any.
func (l *Label) SetOutput(o markup.RenderOutput) {
l.output = o
l.SetMarkup(o.Markup)
}
+// SetUnderlyingOutput sets the output state without changing the label's
+// markup. This is useful for internal use cases where the label is updated
+// separately.
+func (l *Label) SetUnderlyingOutput(o markup.RenderOutput) {
+ l.output = o
+}
+
type ReferenceHighlighter interface {
HighlightReference(ref markup.ReferenceSegment)
}
diff --git a/internal/ui/rich/parser/markup/markup.go b/internal/ui/rich/parser/markup/markup.go
index 872ae86..0b88a65 100644
--- a/internal/ui/rich/parser/markup/markup.go
+++ b/internal/ui/rich/parser/markup/markup.go
@@ -9,7 +9,6 @@ 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"
@@ -21,7 +20,30 @@ import (
var Hyphenate = false
func hyphenate(text string) string {
- return fmt.Sprintf(`%s`, Hyphenate, text)
+ if !Hyphenate {
+ return text
+ }
+ return `` + text + ``
+}
+
+// SubstringSegment slices the given rich text.
+func SubstringSegment(rich text.Rich, seg text.Segment) text.Rich {
+ start, end := seg.Bounds()
+ substring := text.Rich{
+ Content: rich.Content[start:end],
+ Segments: make([]text.Segment, 0, len(rich.Segments)),
+ }
+
+ for _, seg := range rich.Segments {
+ i, j := seg.Bounds()
+
+ // Bound-check.
+ if start <= i && j <= end {
+ substring.Segments = append(substring.Segments, seg)
+ }
+ }
+
+ return substring
}
// RenderOutput is the output of a render.
@@ -79,8 +101,13 @@ func (r RenderOutput) URISegment(uri string) text.Segment {
}
}
+var simpleConfig = RenderConfig{
+ NoMentionLinks: true,
+ NoReferencing: true,
+}
+
func Render(content text.Rich) string {
- return RenderCmplx(content).Markup
+ return RenderCmplxWithConfig(content, simpleConfig).Markup
}
// RenderCmplx renders content into a complete output.
@@ -107,18 +134,19 @@ type RenderConfig struct {
// 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 (c *RenderConfig) SetForegroundAnchor(ctx *gtk.StyleContext) {
+ rgba := ctx.GetColor(gtk.STATE_FLAG_NORMAL)
+ if rgba == nil {
+ return
}
+
+ 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 {
@@ -183,35 +211,19 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
// 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 && !cfg.NoMentionLinks {
+ if mentioner := segment.AsMentioner(); mentioner != nil {
// Render the mention into "cchat://mention:0" or such. Other
// components will take care of showing the information.
- appended.AnchorNU(start, end, fmtSegmentURI(MentionType, len(mentions)))
- hasAnchor = true
+ if !cfg.NoMentionLinks {
+ appended.AnchorNU(start, end, fmtSegmentURI(MentionType, len(mentions)))
+ hasAnchor = true
+ }
// Add the mention segment into the list regardless of hyperlinks.
mentions = append(mentions, MentionSegment{
Segment: segment,
Mentioner: mentioner,
})
-
- // 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 {
@@ -223,18 +235,18 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
// 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 !cfg.NoReferencing && !hasAnchor {
- if reference := segment.AsMessageReferencer(); reference != nil {
+ if reference := segment.AsMessageReferencer(); reference != nil {
+ if !cfg.NoReferencing && !hasAnchor {
// Render the mention into "cchat://reference:0" or such. Other
// components will take care of showing the information.
appended.AnchorNU(start, end, fmtSegmentURI(ReferenceType, len(references)))
-
- // Add the mention segment into the list regardless of hyperlinks.
- references = append(references, ReferenceSegment{
- Segment: segment,
- MessageReferencer: reference,
- })
}
+
+ // Add the mention segment into the list regardless of hyperlinks.
+ references = append(references, ReferenceSegment{
+ Segment: segment,
+ MessageReferencer: reference,
+ })
}
if attributor := segment.AsAttributor(); attributor != nil {
diff --git a/internal/ui/ui.go b/internal/ui/ui.go
index 8092cd6..dbce92e 100644
--- a/internal/ui/ui.go
+++ b/internal/ui/ui.go
@@ -159,7 +159,9 @@ func (app *App) SessionSelected(svc *service.Service, ses *session.Row) {
}
func (app *App) ClearMessenger(ses *session.Row) {
- if app.MessageView.SessionID() == ses.Session.ID() {
+ // No need to try if the window is destroyed already, since its children
+ // will also be destroyed.
+ if !gts.IsClosing() && app.MessageView.SessionID() == ses.Session.ID() {
app.MessageView.Reset()
}
}