mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-03-24 02:49:23 +00:00
minor bug fixes and optimizations
This commit is contained in:
parent
bcd2de2e49
commit
ee041b3cc9
|
@ -22,6 +22,13 @@ var App struct {
|
||||||
*gtk.Application
|
*gtk.Application
|
||||||
Window *handy.ApplicationWindow
|
Window *handy.ApplicationWindow
|
||||||
Throttler *throttler.State
|
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.
|
// 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) {
|
App.Window.Window.Connect("destroy", func(window *handy.ApplicationWindow) {
|
||||||
// Hide the application window.
|
// Hide the application window.
|
||||||
window.Hide()
|
window.Hide()
|
||||||
|
App.closing = true
|
||||||
|
|
||||||
// Let the main loop run once by queueing the stop loop afterwards.
|
// 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
|
// This is to allow the main loop to properly hide the Gtk window
|
||||||
|
|
|
@ -13,22 +13,31 @@ import (
|
||||||
"github.com/pkg/errors"
|
"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{
|
var dskcached = http.Client{
|
||||||
Timeout: 15 * time.Second,
|
Timeout: 15 * time.Second,
|
||||||
Transport: httpcache.NewTransport(
|
Transport: &httpcache.Transport{
|
||||||
diskcache.NewWithDiskv(diskv.New(diskv.Options{
|
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,
|
BasePath: basePath,
|
||||||
TempDir: filepath.Join(basePath, "tmp"),
|
TempDir: filepath.Join(basePath, "tmp"),
|
||||||
PathPerm: 0750,
|
PathPerm: 0750,
|
||||||
FilePerm: 0750,
|
FilePerm: 0750,
|
||||||
Compression: diskv.NewZlibCompressionLevel(5),
|
Compression: diskv.NewZlibCompressionLevel(4),
|
||||||
CacheSizeMax: 0, // 25 MiB in memory
|
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) {
|
func get(ctx context.Context, url string, cached bool) (r *http.Response, err error) {
|
||||||
q, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
q, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package httputil
|
package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
@ -8,6 +9,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
|
@ -19,7 +21,31 @@ import (
|
||||||
"github.com/pkg/errors"
|
"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 {
|
type ImageContainer interface {
|
||||||
primitives.Connector
|
primitives.Connector
|
||||||
|
@ -67,9 +93,9 @@ func AsyncImage(ctx context.Context,
|
||||||
scale = surfaceContainer.GetScaleFactor()
|
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.
|
// Try and guess the MIME type from the URL.
|
||||||
mimeType := mime.TypeByExtension(urlExt(imageURL))
|
mimeType := mime.TypeByExtension(urlExt(imageURL))
|
||||||
|
|
||||||
|
@ -102,7 +128,7 @@ func AsyncImage(ctx context.Context,
|
||||||
|
|
||||||
l, err := gdk.PixbufLoaderNewWithType(fileType)
|
l, err := gdk.PixbufLoaderNewWithType(fileType)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,11 +143,20 @@ func AsyncImage(ctx context.Context,
|
||||||
l.Connect("area-prepared", load)
|
l.Connect("area-prepared", load)
|
||||||
l.Connect("area-updated", 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))
|
log.Error(errors.Wrapf(err, "failed to download %q", imageURL))
|
||||||
// Force close after downloading.
|
// 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 {
|
if err := l.Close(); err != nil {
|
||||||
log.Error(errors.Wrapf(err, "failed to close pixbuf loader for %q", imageURL))
|
log.Error(errors.Wrapf(err, "failed to close pixbuf loader for %q", imageURL))
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ type Container struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer(ctrl container.Controller) *Container {
|
func NewContainer(ctrl container.Controller) *Container {
|
||||||
c := container.NewListContainer(constructor{}, ctrl)
|
c := container.NewListContainer(ctrl, constructors)
|
||||||
primitives.AddClass(c, "compact-conatainer")
|
primitives.AddClass(c, "compact-container")
|
||||||
return &Container{c}
|
return &Container{c}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,12 +33,19 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
gts.ExecAsync(func() { c.ListContainer.DeleteMessageUnsafe(msg) })
|
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)
|
return NewMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (constructor) NewPresendMessage(msg input.PresendMessage) container.PresendMessageRow {
|
func newPresendMessage(
|
||||||
|
msg input.PresendMessage, _ container.MessageRow) container.PresendMessageRow {
|
||||||
|
|
||||||
return NewPresendMessage(msg)
|
return NewPresendMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,33 @@
|
||||||
package compact
|
package compact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"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/container"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
"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/messages/message"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"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/gtk"
|
||||||
"github.com/gotk3/gotk3/pango"
|
"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 {
|
type PresendMessage struct {
|
||||||
message.PresendContainer
|
message.PresendContainer
|
||||||
Message
|
Message
|
||||||
|
@ -17,60 +35,76 @@ type PresendMessage struct {
|
||||||
|
|
||||||
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
|
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
|
||||||
msgc := message.NewPresendContainer(msg)
|
msgc := message.NewPresendContainer(msg)
|
||||||
attachCompact(msgc.GenericContainer)
|
|
||||||
|
|
||||||
return PresendMessage{
|
return PresendMessage{
|
||||||
PresendContainer: msgc,
|
PresendContainer: msgc,
|
||||||
Message: Message{msgc.GenericContainer},
|
Message: wrapMessage(msgc.GenericContainer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
*message.GenericContainer
|
*message.GenericContainer
|
||||||
|
Timestamp *gtk.Label
|
||||||
|
Username *labeluri.Label
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ container.MessageRow = (*Message)(nil)
|
var _ container.MessageRow = (*Message)(nil)
|
||||||
|
|
||||||
func NewMessage(msg cchat.MessageCreate) Message {
|
func NewMessage(msg cchat.MessageCreate) Message {
|
||||||
msgc := message.NewContainer(msg)
|
msgc := wrapMessage(message.NewContainer(msg))
|
||||||
attachCompact(msgc)
|
|
||||||
message.FillContainer(msgc, msg)
|
message.FillContainer(msgc, msg)
|
||||||
|
return msgc
|
||||||
return Message{msgc}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmptyMessage() Message {
|
func NewEmptyMessage() Message {
|
||||||
ct := message.NewEmptyContainer()
|
ct := message.NewEmptyContainer()
|
||||||
attachCompact(ct)
|
return wrapMessage(ct)
|
||||||
|
|
||||||
return Message{ct}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var messageTimeCSS = primitives.PrepareClassCSS("message-time", `
|
func wrapMessage(ct *message.GenericContainer) Message {
|
||||||
.message-time {
|
ts := message.NewTimestamp()
|
||||||
margin-left: 1em;
|
ts.SetVAlign(gtk.ALIGN_START)
|
||||||
margin-right: 1em;
|
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", `
|
// SetReferenceHighlighter sets the reference highlighter into the message.
|
||||||
.message-author {
|
func (m Message) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
|
||||||
margin-right: 0.5em;
|
m.GenericContainer.SetReferenceHighlighter(r)
|
||||||
}
|
m.Username.SetReferenceHighlighter(r)
|
||||||
`)
|
}
|
||||||
|
|
||||||
func attachCompact(container *message.GenericContainer) {
|
func (m Message) UpdateTimestamp(t time.Time) {
|
||||||
container.Timestamp.SetVAlign(gtk.ALIGN_START)
|
m.GenericContainer.UpdateTimestamp(t)
|
||||||
container.Username.SetMaxWidthChars(25)
|
m.Timestamp.SetText(humanize.TimeAgo(t))
|
||||||
container.Username.SetEllipsize(pango.ELLIPSIZE_NONE)
|
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
|
||||||
container.Username.SetLineWrap(true)
|
}
|
||||||
container.Username.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
|
||||||
|
func (m Message) UpdateAuthor(author cchat.Author) {
|
||||||
messageTimeCSS(container.Timestamp)
|
m.GenericContainer.UpdateAuthor(author)
|
||||||
messageAuthorCSS(container.Username)
|
|
||||||
|
cfg := markup.RenderConfig{}
|
||||||
container.PackStart(container.Timestamp, false, false, 0)
|
cfg.NoReferencing = true
|
||||||
container.PackStart(container.Username, false, false, 0)
|
cfg.SetForegroundAnchor(m.ContentBodyStyle)
|
||||||
container.PackStart(container.Content, true, true, 0)
|
|
||||||
container.SetClass("compact")
|
m.Username.SetOutput(markup.RenderCmplxWithConfig(author.Name(), cfg))
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ type Container interface {
|
||||||
|
|
||||||
// CreateMessageUnsafe creates a new message and returns the index that is
|
// CreateMessageUnsafe creates a new message and returns the index that is
|
||||||
// the location the message is added to.
|
// the location the message is added to.
|
||||||
CreateMessageUnsafe(cchat.MessageCreate)
|
CreateMessageUnsafe(cchat.MessageCreate) MessageRow
|
||||||
UpdateMessageUnsafe(cchat.MessageUpdate)
|
UpdateMessageUnsafe(cchat.MessageUpdate)
|
||||||
DeleteMessageUnsafe(cchat.MessageDelete)
|
DeleteMessageUnsafe(cchat.MessageDelete)
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ type Controller interface {
|
||||||
|
|
||||||
// Constructor is an interface for making custom message implementations which
|
// Constructor is an interface for making custom message implementations which
|
||||||
// allows ListContainer to generically work with.
|
// allows ListContainer to generically work with.
|
||||||
type Constructor interface {
|
type Constructor struct {
|
||||||
NewMessage(cchat.MessageCreate) MessageRow
|
NewMessage func(msg cchat.MessageCreate, before MessageRow) MessageRow
|
||||||
NewPresendMessage(input.PresendMessage) PresendMessageRow
|
NewPresendMessage func(msg input.PresendMessage, before MessageRow) PresendMessageRow
|
||||||
}
|
}
|
||||||
|
|
||||||
const ColumnSpacing = 8
|
const ColumnSpacing = 8
|
||||||
|
@ -107,10 +107,19 @@ type messageRow struct {
|
||||||
presend message.PresendContainer // this shouldn't be here but i'm lazy
|
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)
|
var _ Container = (*ListContainer)(nil)
|
||||||
|
|
||||||
func NewListContainer(constr Constructor, ctrl Controller) *ListContainer {
|
func NewListContainer(ctrl Controller, constr Constructor) *ListContainer {
|
||||||
listStore := NewListStore(constr, ctrl)
|
listStore := NewListStore(ctrl, constr)
|
||||||
listStore.ListBox.Show()
|
listStore.ListBox.Show()
|
||||||
|
|
||||||
clamp := handy.ClampNew()
|
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.
|
// TODO: remove useless abstraction (this file).
|
||||||
func (c *ListContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
|
||||||
// Insert the message first.
|
// // CreateMessageUnsafe inserts a message. It does not clean up old messages.
|
||||||
c.ListStore.CreateMessageUnsafe(msg)
|
// 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
|
// CleanMessages cleans up the oldest messages if the user is scrolled to the
|
||||||
// bottom. True is returned if there were changes.
|
// bottom. True is returned if there were changes.
|
||||||
|
|
|
@ -41,61 +41,45 @@ const (
|
||||||
AvatarMargin = 10
|
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 {
|
type Container struct {
|
||||||
*container.ListContainer
|
*container.ListContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer(ctrl container.Controller) *Container {
|
func NewContainer(ctrl container.Controller) *Container {
|
||||||
c := &Container{}
|
c := container.NewListContainer(ctrl, messageConstructors)
|
||||||
c.ListContainer = container.NewListContainer(c, ctrl)
|
|
||||||
|
|
||||||
primitives.AddClass(c, "cozy-container")
|
primitives.AddClass(c, "cozy-container")
|
||||||
return c
|
return &Container{ListContainer: 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) findAuthorID(authorID string) container.MessageRow {
|
func (c *Container) findAuthorID(authorID string) container.MessageRow {
|
||||||
// Search the old author if we have any.
|
// Search the old author if we have any.
|
||||||
return c.ListStore.FindMessage(func(msgc container.MessageRow) bool {
|
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.
|
// Borrow the avatar pixbuf, but only if the avatar URL is the same.
|
||||||
p, ok := lastAuthorMsg.(AvatarPixbufCopier)
|
p, ok := lastAuthorMsg.(AvatarPixbufCopier)
|
||||||
if ok && lastAuthorMsg.AvatarURL() == avatarURL {
|
if ok && lastAuthorMsg.Author().Avatar() == avatarURL {
|
||||||
p.CopyAvatarPixbuf(full.Avatar.Image)
|
if p.CopyAvatarPixbuf(full.Avatar.Image) {
|
||||||
full.Avatar.ManuallySetURL(avatarURL)
|
full.Avatar.ManuallySetURL(avatarURL)
|
||||||
} else {
|
return
|
||||||
// We can't borrow, so we need to fetch it anew.
|
}
|
||||||
full.Avatar.SetURL(avatarURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
// lastMessageIsAuthor removed - assuming index before insertion is harmful.
|
||||||
// Get the offfsetth message from last.
|
|
||||||
var last = c.ListStore.NthMessage((c.ListStore.MessagesLen() - 1) + offset)
|
|
||||||
return gridMessageIsAuthor(last, id, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gridMessageIsAuthor(gridMsg container.MessageRow, id cchat.ID, name string) bool {
|
func gridMessageIsAuthor(gridMsg container.MessageRow, author cchat.Author) bool {
|
||||||
return gridMsg != nil &&
|
if gridMsg == nil {
|
||||||
gridMsg.AuthorID() == id &&
|
return false
|
||||||
gridMsg.AuthorName() == name
|
}
|
||||||
|
leftAuthor := gridMsg.Author()
|
||||||
|
return true &&
|
||||||
|
leftAuthor.ID() == author.ID() &&
|
||||||
|
leftAuthor.Name().String() == author.Name().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
// Create the message in the parent's handler. This handler will also
|
// Create the message in the parent's handler. This handler will also
|
||||||
// wipe old messages.
|
// 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
|
// Did the handler wipe old messages? It will only do so if the user is
|
||||||
// scrolled to the bottom.
|
// scrolled to the bottom.
|
||||||
|
@ -143,24 +143,12 @@ func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||||
c.uncompact(c.FirstMessage())
|
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
|
// If we've prepended the message, then see if we need to collapse the
|
||||||
// second message.
|
// second message.
|
||||||
case c.ListContainer.FirstMessage().ID():
|
if first := c.ListContainer.FirstMessage(); first != nil && first.ID() == msg.ID() {
|
||||||
if sec := c.NthMessage(1); sec != nil {
|
// If the author is the same, then collapse.
|
||||||
// The author is the same; collapse.
|
if sec := c.NthMessage(1); sec != nil && gridMessageIsAuthor(sec, msg.Author()) {
|
||||||
author := msg.Author()
|
c.compact(sec)
|
||||||
if gridMessageIsAuthor(sec, author.ID(), author.Name().String()) {
|
|
||||||
c.compact(sec)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -174,15 +162,17 @@ func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
|
||||||
|
|
||||||
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
|
msgID := msg.ID()
|
||||||
|
|
||||||
// Get the previous and next message before deleting. We'll need them to
|
// Get the previous and next message before deleting. We'll need them to
|
||||||
// evaluate whether we need to change anything.
|
// 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
|
// The function doesn't actually try and re-collapse the bottom message
|
||||||
// when a sandwiched message is deleted. This is fine.
|
// when a sandwiched message is deleted. This is fine.
|
||||||
|
|
||||||
// Delete the message off of the parent's container.
|
// 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
|
// Don't calculate if we don't have any messages, or no messages before
|
||||||
// and after.
|
// and after.
|
||||||
|
@ -190,8 +180,10 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msgAuthorID := msg.Author().ID()
|
||||||
|
|
||||||
// Check if the last message is the author's (relative to i):
|
// 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
|
// If the author is the same, then we don't need to uncollapse the
|
||||||
// message.
|
// message.
|
||||||
return
|
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
|
// If the next message (relative to i) is not the deleted message's
|
||||||
// author, then we don't need to uncollapse it.
|
// author, then we don't need to uncollapse it.
|
||||||
if next.AuthorID() != msg.AuthorID() {
|
if next.Author().ID() != msgAuthorID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,39 +203,30 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
func (c *Container) uncompact(msg container.MessageRow) {
|
func (c *Container) uncompact(msg container.MessageRow) {
|
||||||
// We should only uncompact the message if it's compacted in the first
|
// We should only uncompact the message if it's compacted in the first
|
||||||
// place.
|
// place.
|
||||||
if collapse, ok := msg.(Collapsible); !ok || !collapse.Collapsed() {
|
compact, ok := msg.(*CollapsedMessage)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can't unwrap if the message doesn't implement Unwrapper.
|
|
||||||
uw, ok := msg.(Unwrapper)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the "lengthy" uncollapse process.
|
// Start the "lengthy" uncollapse process.
|
||||||
full := WrapFullMessage(uw.Unwrap())
|
full := WrapFullMessage(compact.Unwrap())
|
||||||
// Update the container to reformat everything including the timestamps.
|
// Update the container to reformat everything including the timestamps.
|
||||||
message.RefreshContainer(full, full.GenericContainer)
|
message.RefreshContainer(full, full.GenericContainer)
|
||||||
// Update the avatar if needed be, since we're now showing it.
|
// 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.
|
// Swap the old next message out for a new one.
|
||||||
c.ListStore.SwapMessage(full)
|
c.ListStore.SwapMessage(full)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) compact(msg container.MessageRow) {
|
func (c *Container) compact(msg container.MessageRow) {
|
||||||
// Exit if the message is already collapsed.
|
full, ok := msg.(*FullMessage)
|
||||||
if collapse, ok := msg.(Collapsible); !ok || collapse.Collapsed() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uw, ok := msg.(Unwrapper)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
compact := WrapCollapsedMessage(uw.Unwrap())
|
compact := WrapCollapsedMessage(full.Unwrap())
|
||||||
message.RefreshContainer(compact, compact.GenericContainer)
|
message.RefreshContainer(compact, compact.GenericContainer)
|
||||||
|
|
||||||
c.ListStore.SwapMessage(compact)
|
c.ListStore.SwapMessage(compact)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
type CollapsedMessage struct {
|
type CollapsedMessage struct {
|
||||||
// Author is still updated normally.
|
// Author is still updated normally.
|
||||||
*message.GenericContainer
|
*message.GenericContainer
|
||||||
|
Timestamp *gtk.Label
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCollapsedMessage(msg cchat.MessageCreate) *CollapsedMessage {
|
func NewCollapsedMessage(msg cchat.MessageCreate) *CollapsedMessage {
|
||||||
|
@ -26,21 +27,23 @@ func NewCollapsedMessage(msg cchat.MessageCreate) *CollapsedMessage {
|
||||||
|
|
||||||
func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
|
func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
|
||||||
// Set Timestamp's padding accordingly to Avatar's.
|
// Set Timestamp's padding accordingly to Avatar's.
|
||||||
gc.Timestamp.SetSizeRequest(AvatarSize, -1)
|
ts := message.NewTimestamp()
|
||||||
gc.Timestamp.SetVAlign(gtk.ALIGN_START)
|
ts.SetSizeRequest(AvatarSize, -1)
|
||||||
gc.Timestamp.SetXAlign(0.5) // middle align
|
ts.SetVAlign(gtk.ALIGN_START)
|
||||||
gc.Timestamp.SetMarginEnd(container.ColumnSpacing)
|
ts.SetXAlign(0.5) // middle align
|
||||||
gc.Timestamp.SetMarginStart(container.ColumnSpacing * 2)
|
ts.SetMarginEnd(container.ColumnSpacing)
|
||||||
|
ts.SetMarginStart(container.ColumnSpacing * 2)
|
||||||
|
|
||||||
// Set Content's padding accordingly to FullMessage's main box.
|
// Set Content's padding accordingly to FullMessage's main box.
|
||||||
gc.Content.ToWidget().SetMarginEnd(container.ColumnSpacing * 2)
|
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.PackStart(gc.Content, true, true, 0)
|
||||||
gc.SetClass("cozy-collapsed")
|
gc.SetClass("cozy-collapsed")
|
||||||
|
|
||||||
return &CollapsedMessage{
|
return &CollapsedMessage{
|
||||||
GenericContainer: gc,
|
GenericContainer: gc,
|
||||||
|
Timestamp: ts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
"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/primitives/roundimage"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
|
"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/cairo"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
@ -27,12 +29,12 @@ type FullMessage struct {
|
||||||
Avatar *Avatar
|
Avatar *Avatar
|
||||||
MainBox *gtk.Box // wraps header and content
|
MainBox *gtk.Box // wraps header and content
|
||||||
|
|
||||||
// Header wraps author and timestamp.
|
Header *labeluri.Label
|
||||||
HeaderBox *gtk.Box
|
timestamp string // markup
|
||||||
}
|
}
|
||||||
|
|
||||||
type AvatarPixbufCopier interface {
|
type AvatarPixbufCopier interface {
|
||||||
CopyAvatarPixbuf(img httputil.SurfaceContainer)
|
CopyAvatarPixbuf(img httputil.SurfaceContainer) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -41,10 +43,6 @@ var (
|
||||||
_ container.MessageRow = (*FullMessage)(nil)
|
_ container.MessageRow = (*FullMessage)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
var boldCSS = primitives.PrepareCSS(`
|
|
||||||
* { font-weight: 600; }
|
|
||||||
`)
|
|
||||||
|
|
||||||
var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", `
|
var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", `
|
||||||
/* Slightly dip down on click */
|
/* Slightly dip down on click */
|
||||||
.cozy-avatar:active {
|
.cozy-avatar:active {
|
||||||
|
@ -63,33 +61,26 @@ func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
func WrapFullMessage(gc *message.GenericContainer) *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 := NewAvatar()
|
||||||
avatar.SetMarginTop(TopFullMargin / 2)
|
avatar.SetMarginTop(TopFullMargin / 2)
|
||||||
avatar.SetMarginStart(container.ColumnSpacing * 2)
|
avatar.SetMarginStart(container.ColumnSpacing * 2)
|
||||||
avatar.Connect("clicked", func(w gtk.IWidget) {
|
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])
|
labeluri.PopoverMentioner(w, output.Input, output.Mentions[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
avatar.Show()
|
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.
|
// Attach the class and CSS for the left avatar.
|
||||||
avatarCSS(avatar)
|
avatarCSS(avatar)
|
||||||
|
|
||||||
// Attach the username style provider.
|
// Attach the username style provider.
|
||||||
primitives.AttachCSS(gc.Username, boldCSS)
|
// 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()
|
|
||||||
|
|
||||||
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
main.PackStart(header, false, false, 0)
|
main.PackStart(header, false, false, 0)
|
||||||
|
@ -108,9 +99,10 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
||||||
|
|
||||||
return &FullMessage{
|
return &FullMessage{
|
||||||
GenericContainer: gc,
|
GenericContainer: gc,
|
||||||
Avatar: avatar,
|
|
||||||
MainBox: main,
|
Avatar: avatar,
|
||||||
HeaderBox: header,
|
MainBox: main,
|
||||||
|
Header: header,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,17 +110,12 @@ func (m *FullMessage) Collapsed() bool { return false }
|
||||||
|
|
||||||
func (m *FullMessage) Unwrap() *message.GenericContainer {
|
func (m *FullMessage) Unwrap() *message.GenericContainer {
|
||||||
// Remove GenericContainer's widgets from the containers.
|
// Remove GenericContainer's widgets from the containers.
|
||||||
m.HeaderBox.Remove(m.Username)
|
m.Header.Destroy()
|
||||||
m.HeaderBox.Remove(m.Timestamp)
|
m.MainBox.Remove(m.Content) // not ours, so don't destroy.
|
||||||
m.MainBox.Remove(m.HeaderBox)
|
|
||||||
m.MainBox.Remove(m.Content)
|
|
||||||
|
|
||||||
// Hide the avatar.
|
|
||||||
m.Avatar.Hide()
|
|
||||||
|
|
||||||
// Remove the message from the grid.
|
// Remove the message from the grid.
|
||||||
m.Remove(m.Avatar)
|
m.Avatar.Destroy()
|
||||||
m.Remove(m.MainBox)
|
m.MainBox.Destroy()
|
||||||
|
|
||||||
// Return after removing.
|
// Return after removing.
|
||||||
return m.GenericContainer
|
return m.GenericContainer
|
||||||
|
@ -136,18 +123,36 @@ func (m *FullMessage) Unwrap() *message.GenericContainer {
|
||||||
|
|
||||||
func (m *FullMessage) UpdateTimestamp(t time.Time) {
|
func (m *FullMessage) UpdateTimestamp(t time.Time) {
|
||||||
m.GenericContainer.UpdateTimestamp(t)
|
m.GenericContainer.UpdateTimestamp(t)
|
||||||
m.Timestamp.SetText(humanize.TimeAgoLong(t))
|
|
||||||
|
m.timestamp = " " +
|
||||||
|
`<span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>`
|
||||||
|
|
||||||
|
// Update the timestamp.
|
||||||
|
m.Header.SetMarkup(m.Header.Output().Markup + m.timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FullMessage) UpdateAuthor(author cchat.Author) {
|
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.GenericContainer.UpdateAuthor(author)
|
||||||
|
m.UpdateAuthorName(author.Name())
|
||||||
m.Avatar.SetURL(author.Avatar())
|
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 = `<span font_weight="600">` + output.Markup + "</span>"
|
||||||
|
|
||||||
|
m.Header.SetMarkup(output.Markup + m.timestamp)
|
||||||
|
m.Header.SetUnderlyingOutput(output)
|
||||||
|
}
|
||||||
|
|
||||||
// CopyAvatarPixbuf sets the pixbuf into the given container. This shares the
|
// CopyAvatarPixbuf sets the pixbuf into the given container. This shares the
|
||||||
// same pixbuf, but gtk.Image should take its own reference from the pixbuf.
|
// 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() {
|
switch img := m.Avatar.Image.GetImage(); img.GetStorageType() {
|
||||||
case gtk.IMAGE_PIXBUF:
|
case gtk.IMAGE_PIXBUF:
|
||||||
dst.SetFromPixbuf(img.GetPixbuf())
|
dst.SetFromPixbuf(img.GetPixbuf())
|
||||||
|
@ -156,7 +161,10 @@ func (m *FullMessage) CopyAvatarPixbuf(dst httputil.SurfaceContainer) {
|
||||||
case gtk.IMAGE_SURFACE:
|
case gtk.IMAGE_SURFACE:
|
||||||
v, _ := img.GetProperty("surface")
|
v, _ := img.GetProperty("surface")
|
||||||
dst.SetFromSurface(v.(*cairo.Surface))
|
dst.SetFromSurface(v.(*cairo.Surface))
|
||||||
|
default:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FullMessage) AttachMenu(items []menu.Item) {
|
func (m *FullMessage) AttachMenu(items []menu.Item) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
@ -35,7 +34,7 @@ type ListStore struct {
|
||||||
messages map[messageKey]*messageRow
|
messages map[messageKey]*messageRow
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListStore(constr Constructor, ctrl Controller) *ListStore {
|
func NewListStore(ctrl Controller, constr Constructor) *ListStore {
|
||||||
listBox, _ := gtk.ListBoxNew()
|
listBox, _ := gtk.ListBoxNew()
|
||||||
listBox.SetSelectionMode(gtk.SELECTION_SINGLE)
|
listBox.SetSelectionMode(gtk.SELECTION_SINGLE)
|
||||||
listBox.Show()
|
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
|
// LatestMessageFrom returns the latest message with the given user ID. This is
|
||||||
// used for the input prompt.
|
// used for the input prompt.
|
||||||
func (c *ListStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
func (c *ListStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
||||||
log.Println("LatestMessageFrom called")
|
|
||||||
|
|
||||||
// FindMessage already looks from the latest messages.
|
// FindMessage already looks from the latest messages.
|
||||||
var msg = c.FindMessage(func(msg MessageRow) bool {
|
var msg = c.FindMessage(func(msg MessageRow) bool {
|
||||||
log.Println("Author:", msg.AuthorName())
|
return msg.Author().ID() == userID
|
||||||
return msg.AuthorID() == userID
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
|
@ -229,25 +225,22 @@ func (c *ListStore) FindMessage(isMessage func(MessageRow) bool) MessageRow {
|
||||||
msg, _ := c.findMessage(false, func(row *messageRow) bool {
|
msg, _ := c.findMessage(false, func(row *messageRow) bool {
|
||||||
return isMessage(row.MessageRow)
|
return isMessage(row.MessageRow)
|
||||||
})
|
})
|
||||||
if msg != nil {
|
return unwrapRow(msg)
|
||||||
return msg.MessageRow
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) nthMessage(n int) *messageRow {
|
func (c *ListStore) nthMessage(n int) *messageRow {
|
||||||
v := primitives.NthChild(c.ListBox, n)
|
v := primitives.NthChild(c.ListBox, n)
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
id := primitives.GetName(v.(primitives.Namer))
|
id := primitives.GetName(v.(primitives.Namer))
|
||||||
return c.message(id, "")
|
return c.message(id, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NthMessage returns the nth message.
|
// NthMessage returns the nth message.
|
||||||
func (c *ListStore) NthMessage(n int) MessageRow {
|
func (c *ListStore) NthMessage(n int) MessageRow {
|
||||||
msg := c.nthMessage(n)
|
return unwrapRow(c.nthMessage(n))
|
||||||
if msg != nil {
|
|
||||||
return msg.MessageRow
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FirstMessage returns the first message.
|
// 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
|
// Message finds the message state in the container. It is not thread-safe. This
|
||||||
// exists for backwards compatibility.
|
// exists for backwards compatibility.
|
||||||
func (c *ListStore) Message(msgID cchat.ID, nonce string) MessageRow {
|
func (c *ListStore) Message(msgID cchat.ID, nonce string) MessageRow {
|
||||||
if m := c.message(msgID, nonce); m != nil {
|
return unwrapRow(c.message(msgID, nonce))
|
||||||
return m.MessageRow
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
|
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
|
// AddPresendMessage inserts an input.PresendMessage into the container and
|
||||||
// returning a wrapped widget interface.
|
// returning a wrapped widget interface.
|
||||||
func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRow {
|
func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRow {
|
||||||
presend := c.Construct.NewPresendMessage(msg)
|
before := c.LastMessage()
|
||||||
|
presend := c.Construct.NewPresendMessage(msg, before)
|
||||||
|
|
||||||
msgc := &messageRow{
|
msgc := &messageRow{
|
||||||
MessageRow: presend,
|
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
|
// unreliable. The index might be off if the message buffer is cleaned up. Don't
|
||||||
// rely on it.
|
// rely on it.
|
||||||
|
|
||||||
func (c *ListStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
func (c *ListStore) CreateMessageUnsafe(msg cchat.MessageCreate) MessageRow {
|
||||||
// Call the event handler last.
|
// Call the event handler last.
|
||||||
defer c.Controller.AuthorEvent(msg.Author())
|
defer c.Controller.AuthorEvent(msg.Author())
|
||||||
|
|
||||||
|
@ -337,33 +328,45 @@ func (c *ListStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
msgc.UpdateTimestamp(msg.Time())
|
msgc.UpdateTimestamp(msg.Time())
|
||||||
|
|
||||||
c.bindMessage(msgc)
|
c.bindMessage(msgc)
|
||||||
return
|
return msgc.MessageRow
|
||||||
}
|
}
|
||||||
|
|
||||||
msgc := &messageRow{
|
|
||||||
MessageRow: c.Construct.NewMessage(msg),
|
|
||||||
}
|
|
||||||
msgTime := msg.Time()
|
msgTime := msg.Time()
|
||||||
|
|
||||||
// Iterate and compare timestamp to find where to insert a message.
|
// Iterate and compare timestamp to find where to insert a message. Note
|
||||||
after, index := c.findMessage(true, func(after *messageRow) bool {
|
// that "before" is the message that will go before the to-be-inserted
|
||||||
return msgTime.After(after.Time())
|
// 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
|
msgc := &messageRow{
|
||||||
// oldest, so we add it to the front of the list.
|
MessageRow: c.Construct.NewMessage(msg, unwrapRow(before)),
|
||||||
if after != nil {
|
}
|
||||||
index++ // insert right after
|
|
||||||
c.ListBox.Insert(msgc.Row(), index)
|
// Add the message. If before is nil, then the to-be-inserted message is the
|
||||||
} else {
|
// earliest message, therefore we prepend it.
|
||||||
|
if before == nil {
|
||||||
index = 0
|
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.
|
// Set the ID into the message map.
|
||||||
c.messages[idKey(msgc.ID())] = msgc
|
c.messages[idKey(msgc.ID())] = msgc
|
||||||
|
|
||||||
c.bindMessage(msgc)
|
c.bindMessage(msgc)
|
||||||
|
|
||||||
|
return msgc.MessageRow
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
|
func (c *ListStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
|
||||||
|
@ -416,8 +419,6 @@ func (c *ListStore) DeleteEarliest(n int) {
|
||||||
id := primitives.GetName(v.(primitives.Namer))
|
id := primitives.GetName(v.(primitives.Namer))
|
||||||
gridMsg := c.message(id, "")
|
gridMsg := c.message(id, "")
|
||||||
|
|
||||||
log.Println("Deleting overflowed message ID from", gridMsg.AuthorName())
|
|
||||||
|
|
||||||
if id := gridMsg.ID(); id != "" {
|
if id := gridMsg.ID(); id != "" {
|
||||||
delete(c.messages, idKey(id))
|
delete(c.messages, idKey(id))
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"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/completion"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
"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/diamondburned/handy"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -20,7 +21,7 @@ import (
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
AddPresendMessage(msg PresendMessage) (onErr func(error))
|
AddPresendMessage(msg PresendMessage) (onErr func(error))
|
||||||
LatestMessageFrom(userID cchat.ID) (messageID cchat.ID, ok bool)
|
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.
|
// 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.replyingID = msgID
|
||||||
f.sendIcon.SetFromIconName(replyButtonIcon, gtk.ICON_SIZE_BUTTON)
|
f.sendIcon.SetFromIconName(replyButtonIcon, gtk.ICON_SIZE_BUTTON)
|
||||||
|
|
||||||
name, ok := f.ctrl.MessageAuthorMarkup(msgID)
|
if author := f.ctrl.MessageAuthor(msgID); author != nil {
|
||||||
if !ok {
|
// Extract the name from the author's rich text and only render the area
|
||||||
name = "message"
|
// 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.
|
// Editable returns whether or not the input field can be edited.
|
||||||
|
|
|
@ -64,14 +64,12 @@ func (f *Field) sendInput() {
|
||||||
}
|
}
|
||||||
|
|
||||||
f.SendMessage(SendMessageData{
|
f.SendMessage(SendMessageData{
|
||||||
time: time.Now().UTC(),
|
time: time.Now().UTC(),
|
||||||
content: text,
|
content: text,
|
||||||
author: f.Username.GetLabel(),
|
author: newAuthor(f),
|
||||||
authorID: f.UserID,
|
nonce: f.generateNonce(),
|
||||||
authorURL: f.Username.GetIconURL(),
|
replyID: f.replyingID,
|
||||||
nonce: f.generateNonce(),
|
files: attachments,
|
||||||
replyID: f.replyingID,
|
|
||||||
files: attachments,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clear the input field after sending.
|
// 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
|
// SendMessageData contains what is to be sent in a message. It behaves
|
||||||
// similarly to a regular CreateMessage.
|
// similarly to a regular CreateMessage.
|
||||||
type SendMessageData struct {
|
type SendMessageData struct {
|
||||||
time time.Time
|
time time.Time
|
||||||
content string
|
content string
|
||||||
author text.Rich
|
author cchat.Author
|
||||||
authorID cchat.ID
|
nonce string
|
||||||
authorURL string // avatar
|
replyID cchat.ID
|
||||||
nonce string
|
files Files
|
||||||
replyID cchat.ID
|
|
||||||
files Files
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cchat.SendableMessage = (*SendMessageData)(nil)
|
var _ cchat.SendableMessage = (*SendMessageData)(nil)
|
||||||
|
@ -130,9 +126,7 @@ type PresendMessage interface {
|
||||||
|
|
||||||
// These methods are reserved for internal use.
|
// These methods are reserved for internal use.
|
||||||
|
|
||||||
Author() text.Rich
|
Author() cchat.Author
|
||||||
AuthorID() string
|
|
||||||
AuthorAvatarURL() string // may be empty
|
|
||||||
Files() []attachment.File
|
Files() []attachment.File
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,12 +136,30 @@ var _ PresendMessage = (*SendMessageData)(nil)
|
||||||
func (s SendMessageData) ID() string { return s.nonce }
|
func (s SendMessageData) ID() string { return s.nonce }
|
||||||
func (s SendMessageData) Time() time.Time { return s.time }
|
func (s SendMessageData) Time() time.Time { return s.time }
|
||||||
func (s SendMessageData) Content() string { return s.content }
|
func (s SendMessageData) Content() string { return s.content }
|
||||||
func (s SendMessageData) Author() text.Rich { return s.author }
|
func (s SendMessageData) Author() cchat.Author { return s.author }
|
||||||
func (s SendMessageData) AuthorID() string { return s.authorID }
|
|
||||||
func (s SendMessageData) AuthorAvatarURL() string { return s.authorURL }
|
|
||||||
func (s SendMessageData) AsNoncer() cchat.Noncer { return s }
|
func (s SendMessageData) AsNoncer() cchat.Noncer { return s }
|
||||||
func (s SendMessageData) Nonce() string { return s.nonce }
|
func (s SendMessageData) Nonce() string { return s.nonce }
|
||||||
func (s SendMessageData) Files() []attachment.File { return s.files }
|
func (s SendMessageData) Files() []attachment.File { return s.files }
|
||||||
func (s SendMessageData) AsAttacher() cchat.Attacher { return s.files }
|
func (s SendMessageData) AsAttacher() cchat.Attacher { return s.files }
|
||||||
func (s SendMessageData) AsReplier() cchat.Replier { return s }
|
func (s SendMessageData) AsReplier() cchat.Replier { return s }
|
||||||
func (s SendMessageData) ReplyingTo() cchat.ID { return s.replyID }
|
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 }
|
||||||
|
|
|
@ -29,7 +29,7 @@ func (evq *eventQueue) Add(fn func()) {
|
||||||
if evq.activated {
|
if evq.activated {
|
||||||
evq.idleQueue = append(evq.idleQueue, fn)
|
evq.idleQueue = append(evq.idleQueue, fn)
|
||||||
} else {
|
} else {
|
||||||
gts.ExecAsync(fn)
|
gts.ExecLater(fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,18 +54,25 @@ func (evq *eventQueue) pop() []func() {
|
||||||
func (evq *eventQueue) Deactivate() {
|
func (evq *eventQueue) Deactivate() {
|
||||||
var popped = evq.pop()
|
var popped = evq.pop()
|
||||||
|
|
||||||
|
const chunkSz = 25
|
||||||
|
|
||||||
// We shouldn't try and run more than a certain amount of callbacks within a
|
// We shouldn't try and run more than a certain amount of callbacks within a
|
||||||
// single loop, as it will freeze up the UI.
|
// single loop, as it will freeze up the UI.
|
||||||
if len(popped) > 25 {
|
for i := 0; i < len(popped); i += chunkSz {
|
||||||
for _, fn := range popped {
|
// Calculate the bounds in chunks.
|
||||||
gts.ExecAsync(fn)
|
start, end := i, min(i+chunkSz, len(popped))
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gts.ExecAsync(func() {
|
gts.ExecLater(func() {
|
||||||
for _, fn := range popped {
|
for _, fn := range popped[start:end] {
|
||||||
fn()
|
fn()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(i, j int) int {
|
||||||
|
if i < j {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return j
|
||||||
}
|
}
|
||||||
|
|
|
@ -366,6 +366,7 @@ func NewMember(member cchat.ListMember) *Member {
|
||||||
|
|
||||||
var noMentionLinks = markup.RenderConfig{
|
var noMentionLinks = markup.RenderConfig{
|
||||||
NoMentionLinks: true,
|
NoMentionLinks: true,
|
||||||
|
NoReferencing: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Member) Update(member cchat.ListMember) {
|
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)
|
m.output = markup.RenderCmplxWithConfig(member.Name(), noMentionLinks)
|
||||||
|
|
||||||
txt := strings.Builder{}
|
txt := strings.Builder{}
|
||||||
txt.WriteString(fmt.Sprintf(
|
txt.WriteString(fmt.Sprintf(
|
||||||
`<span color="#%06X">●</span> %s`,
|
`<span color="#%06X" size="large">●</span> %s`,
|
||||||
statusColors(member.Status()), m.output.Markup,
|
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.
|
// Popup pops up the mention popover if any.
|
||||||
func (m *Member) Popup(evq EventQueuer) {
|
func (m *Member) Popup(evq EventQueuer) {
|
||||||
if len(m.output.Mentions) > 0 {
|
if len(m.output.Mentions) == 0 {
|
||||||
p := labeluri.NewPopoverMentioner(m, m.output.Input, m.output.Mentions[0])
|
return
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func statusColors(status cchat.Status) uint32 {
|
||||||
|
|
50
internal/ui/messages/message/author.go
Normal file
50
internal/ui/messages/message/author.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -4,28 +4,22 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"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"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
"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"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
|
"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/diamondburned/cchat/text"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/gotk3/gotk3/pango"
|
"github.com/gotk3/gotk3/pango"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Container interface {
|
type Container interface {
|
||||||
ID() string
|
ID() cchat.ID
|
||||||
Time() time.Time
|
Time() time.Time
|
||||||
AuthorID() string
|
Author() cchat.Author
|
||||||
AuthorName() string
|
|
||||||
AuthorMarkup() string
|
|
||||||
AvatarURL() string // avatar
|
|
||||||
Nonce() string
|
Nonce() string
|
||||||
|
|
||||||
UpdateAuthor(cchat.Author)
|
UpdateAuthor(cchat.Author)
|
||||||
UpdateAuthorName(text.Rich)
|
|
||||||
UpdateContent(c text.Rich, edited bool)
|
UpdateContent(c text.Rich, edited bool)
|
||||||
UpdateTimestamp(time.Time)
|
UpdateTimestamp(time.Time)
|
||||||
}
|
}
|
||||||
|
@ -40,8 +34,6 @@ func FillContainer(c Container, msg cchat.MessageCreate) {
|
||||||
// RefreshContainer sets the container's contents to the one from
|
// RefreshContainer sets the container's contents to the one from
|
||||||
// GenericContainer. This is mainly used for transferring between different
|
// GenericContainer. This is mainly used for transferring between different
|
||||||
// containers.
|
// containers.
|
||||||
//
|
|
||||||
// Right now, this only works with Timestamp, as that's the only state tracked.
|
|
||||||
func RefreshContainer(c Container, gc *GenericContainer) {
|
func RefreshContainer(c Container, gc *GenericContainer) {
|
||||||
c.UpdateTimestamp(gc.time)
|
c.UpdateTimestamp(gc.time)
|
||||||
}
|
}
|
||||||
|
@ -53,40 +45,20 @@ type GenericContainer struct {
|
||||||
row *gtk.ListBoxRow // contains Box
|
row *gtk.ListBoxRow // contains Box
|
||||||
class string
|
class string
|
||||||
|
|
||||||
id string
|
id string
|
||||||
time time.Time
|
time time.Time
|
||||||
authorID string
|
author Author
|
||||||
authorName string
|
nonce string
|
||||||
avatarURL string // avatar
|
|
||||||
nonce string
|
|
||||||
|
|
||||||
Timestamp *gtk.Label
|
Content *gtk.Box
|
||||||
Username *labeluri.Label
|
ContentBody *labeluri.Label
|
||||||
Content gtk.IWidget // conceal widget implementation
|
ContentBodyStyle *gtk.StyleContext
|
||||||
|
|
||||||
contentBox *gtk.Box // basically what is in Content
|
|
||||||
ContentBody *labeluri.Label
|
|
||||||
|
|
||||||
menuItems []menu.Item
|
menuItems []menu.Item
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Container = (*GenericContainer)(nil)
|
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
|
// NewContainer creates a new message container with the given ID and nonce. It
|
||||||
// does not update the widgets, so FillContainer should be called afterwards.
|
// does not update the widgets, so FillContainer should be called afterwards.
|
||||||
func NewContainer(msg cchat.MessageCreate) *GenericContainer {
|
func NewContainer(msg cchat.MessageCreate) *GenericContainer {
|
||||||
|
@ -94,24 +66,12 @@ func NewContainer(msg cchat.MessageCreate) *GenericContainer {
|
||||||
c.id = msg.ID()
|
c.id = msg.ID()
|
||||||
c.time = msg.Time()
|
c.time = msg.Time()
|
||||||
c.nonce = msg.Nonce()
|
c.nonce = msg.Nonce()
|
||||||
c.authorID = msg.Author().ID()
|
c.author.Update(msg.Author())
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmptyContainer() *GenericContainer {
|
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 := labeluri.NewLabel(text.Rich{})
|
||||||
ctbody.SetVExpand(true)
|
ctbody.SetVExpand(true)
|
||||||
ctbody.SetHAlign(gtk.ALIGN_START)
|
ctbody.SetHAlign(gtk.ALIGN_START)
|
||||||
|
@ -123,6 +83,9 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
ctbody.SetTrackVisitedLinks(false)
|
ctbody.SetTrackVisitedLinks(false)
|
||||||
ctbody.Show()
|
ctbody.Show()
|
||||||
|
|
||||||
|
ctbodyStyle, _ := ctbody.GetStyleContext()
|
||||||
|
ctbodyStyle.AddClass("message-content")
|
||||||
|
|
||||||
// Wrap the content label inside a content box.
|
// Wrap the content label inside a content box.
|
||||||
ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
ctbox.SetHExpand(true)
|
ctbox.SetHExpand(true)
|
||||||
|
@ -135,24 +98,15 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
row, _ := gtk.ListBoxRowNew()
|
row, _ := gtk.ListBoxRowNew()
|
||||||
row.Add(box)
|
row.Add(box)
|
||||||
row.Show()
|
row.Show()
|
||||||
|
|
||||||
// Add CSS classes.
|
|
||||||
primitives.AddClass(ts, "message-time")
|
|
||||||
primitives.AddClass(row, "message-row")
|
primitives.AddClass(row, "message-row")
|
||||||
primitives.AddClass(user, "message-author")
|
|
||||||
primitives.AddClass(ctbody, "message-content")
|
|
||||||
timestampCSS(ts)
|
|
||||||
authorCSS(ts)
|
|
||||||
|
|
||||||
gc := &GenericContainer{
|
gc := &GenericContainer{
|
||||||
Box: box,
|
Box: box,
|
||||||
row: row,
|
row: row,
|
||||||
|
|
||||||
Timestamp: ts,
|
Content: ctbox,
|
||||||
Username: user,
|
ContentBody: ctbody,
|
||||||
Content: ctbox,
|
ContentBodyStyle: ctbodyStyle,
|
||||||
contentBox: ctbox,
|
|
||||||
ContentBody: ctbody,
|
|
||||||
|
|
||||||
// Time is important, as it is used to sort messages, so we have to be
|
// Time is important, as it is used to sort messages, so we have to be
|
||||||
// careful with this.
|
// careful with this.
|
||||||
|
@ -183,7 +137,6 @@ func (m *GenericContainer) SetClass(class string) {
|
||||||
|
|
||||||
// SetReferenceHighlighter sets the reference highlighter into the message.
|
// SetReferenceHighlighter sets the reference highlighter into the message.
|
||||||
func (m *GenericContainer) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
|
func (m *GenericContainer) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
|
||||||
m.Username.SetReferenceHighlighter(r)
|
|
||||||
m.ContentBody.SetReferenceHighlighter(r)
|
m.ContentBody.SetReferenceHighlighter(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,20 +148,8 @@ func (m *GenericContainer) Time() time.Time {
|
||||||
return m.time
|
return m.time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) AuthorID() string {
|
func (m *GenericContainer) Author() cchat.Author {
|
||||||
return m.authorID
|
return m.author
|
||||||
}
|
|
||||||
|
|
||||||
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) Nonce() string {
|
func (m *GenericContainer) Nonce() string {
|
||||||
|
@ -217,23 +158,10 @@ func (m *GenericContainer) Nonce() string {
|
||||||
|
|
||||||
func (m *GenericContainer) UpdateTimestamp(t time.Time) {
|
func (m *GenericContainer) UpdateTimestamp(t time.Time) {
|
||||||
m.time = t
|
m.time = t
|
||||||
m.Timestamp.SetText(humanize.TimeAgo(t))
|
|
||||||
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) UpdateAuthor(author cchat.Author) {
|
func (m *GenericContainer) UpdateAuthor(author cchat.Author) {
|
||||||
m.authorID = author.ID()
|
m.author.Update(author)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
|
func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
|
||||||
|
|
|
@ -35,14 +35,10 @@ type GenericPresendContainer struct {
|
||||||
var _ PresendContainer = (*GenericPresendContainer)(nil)
|
var _ PresendContainer = (*GenericPresendContainer)(nil)
|
||||||
|
|
||||||
func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
|
func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
|
||||||
return WrapPresendContainer(NewEmptyContainer(), msg)
|
c := NewEmptyContainer()
|
||||||
}
|
|
||||||
|
|
||||||
func WrapPresendContainer(c *GenericContainer, msg input.PresendMessage) *GenericPresendContainer {
|
|
||||||
c.nonce = msg.Nonce()
|
c.nonce = msg.Nonce()
|
||||||
c.authorID = msg.AuthorID()
|
c.UpdateAuthor(msg.Author())
|
||||||
c.UpdateTimestamp(msg.Time())
|
c.UpdateTimestamp(msg.Time())
|
||||||
c.UpdateAuthorName(msg.Author())
|
|
||||||
|
|
||||||
p := &GenericPresendContainer{
|
p := &GenericPresendContainer{
|
||||||
GenericContainer: c,
|
GenericContainer: c,
|
||||||
|
@ -56,7 +52,7 @@ func WrapPresendContainer(c *GenericContainer, msg input.PresendMessage) *Generi
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericPresendContainer) SetSensitive(sensitive bool) {
|
func (m *GenericPresendContainer) SetSensitive(sensitive bool) {
|
||||||
m.contentBox.SetSensitive(sensitive)
|
m.Content.SetSensitive(sensitive)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericPresendContainer) SetDone(id string) {
|
func (m *GenericPresendContainer) SetDone(id string) {
|
||||||
|
@ -68,13 +64,13 @@ func (m *GenericPresendContainer) SetDone(id string) {
|
||||||
// free it from memory.
|
// free it from memory.
|
||||||
m.presend = nil
|
m.presend = nil
|
||||||
m.uploads = nil
|
m.uploads = nil
|
||||||
m.contentBox.SetTooltipText("")
|
m.Content.SetTooltipText("")
|
||||||
|
|
||||||
// Remove everything in the content box.
|
// Remove everything in the content box.
|
||||||
m.clearBox()
|
m.clearBox()
|
||||||
|
|
||||||
// Re-add the content label.
|
// 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.
|
// Set the sensitivity from false in SetLoading back to true.
|
||||||
m.SetSensitive(true)
|
m.SetSensitive(true)
|
||||||
|
@ -82,18 +78,18 @@ func (m *GenericPresendContainer) SetDone(id string) {
|
||||||
|
|
||||||
func (m *GenericPresendContainer) SetLoading() {
|
func (m *GenericPresendContainer) SetLoading() {
|
||||||
m.SetSensitive(false)
|
m.SetSensitive(false)
|
||||||
m.contentBox.SetTooltipText("")
|
m.Content.SetTooltipText("")
|
||||||
|
|
||||||
// Clear everything inside the content container.
|
// Clear everything inside the content container.
|
||||||
m.clearBox()
|
m.clearBox()
|
||||||
|
|
||||||
// Add the content label.
|
// Add the content label.
|
||||||
m.contentBox.Add(m.ContentBody)
|
m.Content.Add(m.ContentBody)
|
||||||
|
|
||||||
// Add the attachment progress box back in, if any.
|
// Add the attachment progress box back in, if any.
|
||||||
if m.uploads != nil {
|
if m.uploads != nil {
|
||||||
m.uploads.Show() // show the bars
|
m.uploads.Show() // show the bars
|
||||||
m.contentBox.Add(m.uploads)
|
m.Content.Add(m.uploads)
|
||||||
}
|
}
|
||||||
|
|
||||||
if content := m.presend.Content(); content != "" {
|
if content := m.presend.Content(); content != "" {
|
||||||
|
@ -106,13 +102,13 @@ func (m *GenericPresendContainer) SetLoading() {
|
||||||
|
|
||||||
func (m *GenericPresendContainer) SetSentError(err error) {
|
func (m *GenericPresendContainer) SetSentError(err error) {
|
||||||
m.SetSensitive(true) // allow events incl right clicks
|
m.SetSensitive(true) // allow events incl right clicks
|
||||||
m.contentBox.SetTooltipText(err.Error())
|
m.Content.SetTooltipText(err.Error())
|
||||||
|
|
||||||
// Remove everything again.
|
// Remove everything again.
|
||||||
m.clearBox()
|
m.clearBox()
|
||||||
|
|
||||||
// Re-add the label.
|
// Re-add the label.
|
||||||
m.contentBox.Add(m.ContentBody)
|
m.Content.Add(m.ContentBody)
|
||||||
|
|
||||||
// Style the label appropriately by making it red.
|
// Style the label appropriately by making it red.
|
||||||
var content = EmptyContentPlaceholder
|
var content = EmptyContentPlaceholder
|
||||||
|
@ -132,13 +128,10 @@ func (m *GenericPresendContainer) SetSentError(err error) {
|
||||||
))
|
))
|
||||||
|
|
||||||
errl.Show()
|
errl.Show()
|
||||||
m.contentBox.Add(errl)
|
m.Content.Add(errl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearBox clears everything inside the content container.
|
// clearBox clears everything inside the content container.
|
||||||
func (m *GenericPresendContainer) clearBox() {
|
func (m *GenericPresendContainer) clearBox() {
|
||||||
primitives.ForeachChild(m.contentBox, func(v interface{}) (stop bool) {
|
primitives.RemoveChildren(m.Content)
|
||||||
m.contentBox.Remove(v.(gtk.IWidget))
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
27
internal/ui/messages/message/timestamp.go
Normal file
27
internal/ui/messages/message/timestamp.go
Normal file
|
@ -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
|
||||||
|
}
|
25
internal/ui/messages/message/username.go
Normal file
25
internal/ui/messages/message/username.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -14,8 +14,10 @@ type FaceView struct {
|
||||||
gtk.Stack
|
gtk.Stack
|
||||||
placeholder gtk.IWidget
|
placeholder gtk.IWidget
|
||||||
|
|
||||||
Face *Container
|
face *Container
|
||||||
Loading *Spinner
|
loading *Spinner
|
||||||
|
parent gtk.IWidget
|
||||||
|
empty gtk.IWidget
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(parent gtk.IWidget, placeholder gtk.IWidget) *FaceView {
|
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, _ := gtk.StackNew()
|
||||||
stack.SetTransitionDuration(55)
|
stack.SetTransitionDuration(55)
|
||||||
stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
|
stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
|
||||||
stack.AddNamed(parent, "main")
|
stack.Add(parent)
|
||||||
stack.AddNamed(placeholder, "placeholder")
|
stack.Add(c)
|
||||||
stack.AddNamed(c, "face")
|
stack.Add(s)
|
||||||
stack.AddNamed(s, "loading")
|
stack.Add(b)
|
||||||
stack.AddNamed(b, "empty")
|
|
||||||
|
|
||||||
// Show placeholder by default.
|
// 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.
|
// Reset brings the view to an empty box.
|
||||||
func (v *FaceView) Reset() {
|
func (v *FaceView) Reset() {
|
||||||
v.Loading.Spinner.Stop()
|
v.loading.Spinner.Stop()
|
||||||
v.Stack.SetVisibleChildName("empty")
|
v.Stack.SetVisibleChild(v.empty)
|
||||||
v.ensurePlaceholderDestroyed()
|
v.ensurePlaceholderDestroyed()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FaceView) SetMain() {
|
func (v *FaceView) SetMain() {
|
||||||
v.Loading.Spinner.Stop()
|
v.loading.Spinner.Stop()
|
||||||
v.Stack.SetVisibleChildName("main")
|
v.Stack.SetVisibleChild(v.parent)
|
||||||
v.ensurePlaceholderDestroyed()
|
v.ensurePlaceholderDestroyed()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FaceView) SetLoading() {
|
func (v *FaceView) SetLoading() {
|
||||||
v.Loading.Spinner.Start()
|
v.loading.Spinner.Start()
|
||||||
v.Stack.SetVisibleChildName("loading")
|
v.Stack.SetVisibleChild(v.loading)
|
||||||
v.ensurePlaceholderDestroyed()
|
v.ensurePlaceholderDestroyed()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FaceView) SetError(err error) {
|
func (v *FaceView) SetError(err error) {
|
||||||
v.Face.SetError(err)
|
v.face.SetError(err)
|
||||||
v.Stack.SetVisibleChildName("face")
|
v.Stack.SetVisibleChild(v.face)
|
||||||
v.ensurePlaceholderDestroyed()
|
v.ensurePlaceholderDestroyed()
|
||||||
v.Loading.Spinner.Stop()
|
v.loading.Spinner.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FaceView) ensurePlaceholderDestroyed() {
|
func (v *FaceView) ensurePlaceholderDestroyed() {
|
||||||
|
@ -74,7 +84,7 @@ func (v *FaceView) ensurePlaceholderDestroyed() {
|
||||||
if v.placeholder != nil {
|
if v.placeholder != nil {
|
||||||
// Safely remove the placeholder from the stack.
|
// Safely remove the placeholder from the stack.
|
||||||
if v.Stack.GetVisibleChildName() == "placeholder" {
|
if v.Stack.GetVisibleChildName() == "placeholder" {
|
||||||
v.Stack.SetVisibleChildName("empty")
|
v.Stack.SetVisibleChild(v.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the placeholder widget.
|
// Remove the placeholder widget.
|
||||||
|
|
|
@ -232,8 +232,8 @@ func (v *View) Reset() {
|
||||||
|
|
||||||
// reset resets the message view, but does not change visible containers.
|
// reset resets the message view, but does not change visible containers.
|
||||||
func (v *View) reset() {
|
func (v *View) reset() {
|
||||||
v.Header.Reset() // Reset the header.
|
|
||||||
v.state.Reset() // Reset the state variables.
|
v.state.Reset() // Reset the state variables.
|
||||||
|
v.Header.Reset() // Reset the header.
|
||||||
v.Typing.Reset() // Reset the typing state.
|
v.Typing.Reset() // Reset the typing state.
|
||||||
v.InputView.Reset() // Reset the input.
|
v.InputView.Reset() // Reset the input.
|
||||||
v.MemberList.Reset() // Reset the member list.
|
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, "")
|
msg := v.Container.Message(msgID, "")
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
return "", false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg.AuthorMarkup(), true
|
return msg.Author()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestMessageFrom returns the last message ID with that author.
|
// LatestMessageFrom returns the last message ID with that author.
|
||||||
|
|
|
@ -26,13 +26,13 @@ type Container interface {
|
||||||
var _ Container = (*gtk.Container)(nil)
|
var _ Container = (*gtk.Container)(nil)
|
||||||
|
|
||||||
func RemoveChildren(w Container) {
|
func RemoveChildren(w Container) {
|
||||||
type destroyer interface {
|
// type destroyer interface {
|
||||||
Destroy()
|
// Destroy()
|
||||||
}
|
// }
|
||||||
|
|
||||||
children := w.GetChildren()
|
w.GetChildren().FreeFull(func(child interface{}) {
|
||||||
children.Foreach(func(child interface{}) { w.Remove(child.(gtk.IWidget)) })
|
w.Remove(child.(gtk.IWidget))
|
||||||
children.Free()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChildrenLen gets the total count of children for the given container.
|
// 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()
|
children := w.GetChildren()
|
||||||
defer children.Free()
|
defer children.Free()
|
||||||
|
|
||||||
|
// Bound check!
|
||||||
|
if n < 0 || int(children.Length()) >= n {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return children.Data()
|
return children.Data()
|
||||||
}
|
}
|
||||||
|
|
||||||
return children.NthData(uint(n))
|
return children.NthData(uint(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,7 +330,9 @@ func PrepareClassCSS(class, css string) (attach func(StyleContexter)) {
|
||||||
return func(ctx StyleContexter) {
|
return func(ctx StyleContexter) {
|
||||||
s, _ := ctx.GetStyleContext()
|
s, _ := ctx.GetStyleContext()
|
||||||
s.AddProvider(prov, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
s.AddProvider(prov, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||||
s.AddClass(class)
|
if class != "" {
|
||||||
|
s.AddClass(class)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,12 +82,20 @@ func (l *Label) Output() markup.RenderOutput {
|
||||||
return l.output
|
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) {
|
func (l *Label) SetOutput(o markup.RenderOutput) {
|
||||||
l.output = o
|
l.output = o
|
||||||
l.SetMarkup(o.Markup)
|
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 {
|
type ReferenceHighlighter interface {
|
||||||
HighlightReference(ref markup.ReferenceSegment)
|
HighlightReference(ref markup.ReferenceSegment)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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/attrmap"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/hl"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/hl"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
@ -21,7 +20,30 @@ import (
|
||||||
var Hyphenate = false
|
var Hyphenate = false
|
||||||
|
|
||||||
func hyphenate(text string) string {
|
func hyphenate(text string) string {
|
||||||
return fmt.Sprintf(`<span insert_hyphens="%t">%s</span>`, Hyphenate, text)
|
if !Hyphenate {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return `<span insert_hyphens="true">` + text + `</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// 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 {
|
func Render(content text.Rich) string {
|
||||||
return RenderCmplx(content).Markup
|
return RenderCmplxWithConfig(content, simpleConfig).Markup
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderCmplx renders content into a complete output.
|
// 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
|
// SetForegroundAnchor sets the AnchorColor of the render config to be that of
|
||||||
// the regular text foreground color.
|
// the regular text foreground color.
|
||||||
func (c *RenderConfig) SetForegroundAnchor(styler primitives.StyleContexter) {
|
func (c *RenderConfig) SetForegroundAnchor(ctx *gtk.StyleContext) {
|
||||||
styleCtx, _ := styler.GetStyleContext()
|
rgba := ctx.GetColor(gtk.STATE_FLAG_NORMAL)
|
||||||
|
if rgba == nil {
|
||||||
if rgba := styleCtx.GetColor(gtk.STATE_FLAG_NORMAL); 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
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
|
// 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
|
// segment to also highlight the full mention as well as make the
|
||||||
// padding part of the hyperlink.
|
// 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
|
// Render the mention into "cchat://mention:0" or such. Other
|
||||||
// components will take care of showing the information.
|
// components will take care of showing the information.
|
||||||
appended.AnchorNU(start, end, fmtSegmentURI(MentionType, len(mentions)))
|
if !cfg.NoMentionLinks {
|
||||||
hasAnchor = true
|
appended.AnchorNU(start, end, fmtSegmentURI(MentionType, len(mentions)))
|
||||||
|
hasAnchor = true
|
||||||
|
}
|
||||||
|
|
||||||
// Add the mention segment into the list regardless of hyperlinks.
|
// Add the mention segment into the list regardless of hyperlinks.
|
||||||
mentions = append(mentions, MentionSegment{
|
mentions = append(mentions, MentionSegment{
|
||||||
Segment: segment,
|
Segment: segment,
|
||||||
Mentioner: mentioner,
|
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 {
|
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
|
// Don't use AnchorColor for the link, as we're technically just
|
||||||
// borrowing the anchor tag for its use. We should also prefer the
|
// borrowing the anchor tag for its use. We should also prefer the
|
||||||
// username popover (Mention) over this.
|
// 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
|
// Render the mention into "cchat://reference:0" or such. Other
|
||||||
// components will take care of showing the information.
|
// components will take care of showing the information.
|
||||||
appended.AnchorNU(start, end, fmtSegmentURI(ReferenceType, len(references)))
|
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 {
|
if attributor := segment.AsAttributor(); attributor != nil {
|
||||||
|
|
|
@ -159,7 +159,9 @@ func (app *App) SessionSelected(svc *service.Service, ses *session.Row) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) ClearMessenger(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()
|
app.MessageView.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue