mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-11-28 23:45:42 +00:00
added proper referencing, dep update, bug fixes
This commit is contained in:
parent
8afbd156bc
commit
e2b316a4df
4
go.mod
4
go.mod
|
|
@ -12,8 +12,8 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20201230
|
||||||
require (
|
require (
|
||||||
github.com/Xuanwo/go-locale v1.0.0
|
github.com/Xuanwo/go-locale v1.0.0
|
||||||
github.com/alecthomas/chroma v0.7.3
|
github.com/alecthomas/chroma v0.7.3
|
||||||
github.com/diamondburned/cchat v0.3.15
|
github.com/diamondburned/cchat v0.3.17
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20210101084233-d8599a528770
|
github.com/diamondburned/cchat-discord v0.0.0-20210102085253-a691813b9041
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db
|
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db
|
||||||
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828
|
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828
|
||||||
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374
|
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374
|
||||||
|
|
|
||||||
8
go.sum
8
go.sum
|
|
@ -60,6 +60,8 @@ github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJ
|
||||||
github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
github.com/diamondburned/cchat v0.3.15 h1:BJf8ZiRtDWTGMtQ3QqjNU0H+784WSrkJEpFGkKY5gEw=
|
github.com/diamondburned/cchat v0.3.15 h1:BJf8ZiRtDWTGMtQ3QqjNU0H+784WSrkJEpFGkKY5gEw=
|
||||||
github.com/diamondburned/cchat v0.3.15/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
github.com/diamondburned/cchat v0.3.15/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.3.17 h1:pGwas8Y0SBU7yg4EQ/MvrbqZhrnRhPBYm1AiRsL147s=
|
||||||
|
github.com/diamondburned/cchat v0.3.17/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d h1:n61DxLdX7nPj7KA1N/azaR8wa0pnDBDT6Yi1seOsBWM=
|
github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d h1:n61DxLdX7nPj7KA1N/azaR8wa0pnDBDT6Yi1seOsBWM=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
|
github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af h1:pTdxsrVSYCdraGormbu1t8uQJMe/OD/ZIz9KljDWAvc=
|
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af h1:pTdxsrVSYCdraGormbu1t8uQJMe/OD/ZIz9KljDWAvc=
|
||||||
|
|
@ -72,6 +74,12 @@ github.com/diamondburned/cchat-discord v0.0.0-20201231025836-96e97aa11705 h1:g0h
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20201231025836-96e97aa11705/go.mod h1:rFBGZYLq0g6Pb/WGN/K0++kXrhCYlQQ1nc2FX4r8CO0=
|
github.com/diamondburned/cchat-discord v0.0.0-20201231025836-96e97aa11705/go.mod h1:rFBGZYLq0g6Pb/WGN/K0++kXrhCYlQQ1nc2FX4r8CO0=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20210101084233-d8599a528770 h1:2BeUPBxGYZm3QioWYw9/4PCJAA2x/I8JTYWS4dmqc3o=
|
github.com/diamondburned/cchat-discord v0.0.0-20210101084233-d8599a528770 h1:2BeUPBxGYZm3QioWYw9/4PCJAA2x/I8JTYWS4dmqc3o=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20210101084233-d8599a528770/go.mod h1:Kv+sNS+3UHHDqiuwEkmfaY8nDr3LPryRzCHaV1E6VeM=
|
github.com/diamondburned/cchat-discord v0.0.0-20210101084233-d8599a528770/go.mod h1:Kv+sNS+3UHHDqiuwEkmfaY8nDr3LPryRzCHaV1E6VeM=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210101223535-ef4cb5318534 h1:v11tEMATMxKhoSzJjvfGgI1AGuxilzQ3xJxVO2tOQ0w=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210101223535-ef4cb5318534/go.mod h1:KL3i+ER58BrJ8JBkpy6WQ0mDZdlkgz7KWm3Ex7i6Mk0=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210102040711-73b0d3f39c41 h1:cUV4BBN8w1Mfp3U2JFyA2R9yyTO6bkqC2wR1rqRYXSg=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210102040711-73b0d3f39c41/go.mod h1:KL3i+ER58BrJ8JBkpy6WQ0mDZdlkgz7KWm3Ex7i6Mk0=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210102085253-a691813b9041 h1:ZTovoKIyXiK5VFRTVrY6YWHIFR5x98u9Q+k9rMjZzvg=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210102085253-a691813b9041/go.mod h1:KL3i+ER58BrJ8JBkpy6WQ0mDZdlkgz7KWm3Ex7i6Mk0=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg=
|
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
|
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=
|
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,11 @@ func DoAfter(d time.Duration, f func()) {
|
||||||
|
|
||||||
// DoAfterMs calls f after the given ms in the Gtk main loop.
|
// DoAfterMs calls f after the given ms in the Gtk main loop.
|
||||||
func DoAfterMs(ms uint, f func()) {
|
func DoAfterMs(ms uint, f func()) {
|
||||||
glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, f)
|
if secs := ms / 1000; secs*1000 == ms {
|
||||||
|
glib.TimeoutSecondsAddPriority(secs, glib.PRIORITY_HIGH_IDLE, f)
|
||||||
|
} else {
|
||||||
|
glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
|
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
|
||||||
|
|
@ -201,7 +205,15 @@ func AfterFunc(d time.Duration, f func()) (stop func()) {
|
||||||
|
|
||||||
// AfterMsFunc is similar to AfterFunc but takes in milliseconds instead.
|
// AfterMsFunc is similar to AfterFunc but takes in milliseconds instead.
|
||||||
func AfterMsFunc(ms uint, f func()) (stop func()) {
|
func AfterMsFunc(ms uint, f func()) (stop func()) {
|
||||||
h := glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, func() bool { f(); return true })
|
fn := func() bool { f(); return true }
|
||||||
|
|
||||||
|
var h glib.SourceHandle
|
||||||
|
if secs := ms / 1000; secs*1000 == ms {
|
||||||
|
h = glib.TimeoutSecondsAddPriority(secs, glib.PRIORITY_HIGH_IDLE, fn)
|
||||||
|
} else {
|
||||||
|
h = glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, fn)
|
||||||
|
}
|
||||||
|
|
||||||
return func() { glib.SourceRemove(h) }
|
return func() { glib.SourceRemove(h) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,36 +9,36 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
*container.GridContainer
|
*container.ListContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer(ctrl container.Controller) *Container {
|
func NewContainer(ctrl container.Controller) *Container {
|
||||||
c := container.NewGridContainer(constructor{}, ctrl)
|
c := container.NewListContainer(constructor{}, ctrl)
|
||||||
primitives.AddClass(c, "compact-conatainer")
|
primitives.AddClass(c, "compact-conatainer")
|
||||||
return &Container{c}
|
return &Container{c}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
c.GridContainer.CreateMessageUnsafe(msg)
|
c.ListContainer.CreateMessageUnsafe(msg)
|
||||||
c.GridContainer.CleanMessages()
|
c.ListContainer.CleanMessages()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
|
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
|
||||||
gts.ExecAsync(func() { c.GridContainer.UpdateMessageUnsafe(msg) })
|
gts.ExecAsync(func() { c.ListContainer.UpdateMessageUnsafe(msg) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
gts.ExecAsync(func() { c.GridContainer.DeleteMessageUnsafe(msg) })
|
gts.ExecAsync(func() { c.ListContainer.DeleteMessageUnsafe(msg) })
|
||||||
}
|
}
|
||||||
|
|
||||||
type constructor struct{}
|
type constructor struct{}
|
||||||
|
|
||||||
func (constructor) NewMessage(msg cchat.MessageCreate) container.GridMessage {
|
func (constructor) NewMessage(msg cchat.MessageCreate) container.MessageRow {
|
||||||
return NewMessage(msg)
|
return NewMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (constructor) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
|
func (constructor) NewPresendMessage(msg input.PresendMessage) container.PresendMessageRow {
|
||||||
return NewPresendMessage(msg)
|
return NewPresendMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import (
|
||||||
"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/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
"github.com/gotk3/gotk3/pango"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PresendMessage struct {
|
type PresendMessage struct {
|
||||||
|
|
@ -14,7 +16,8 @@ type PresendMessage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
|
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
|
||||||
var msgc = message.NewPresendContainer(msg)
|
msgc := message.NewPresendContainer(msg)
|
||||||
|
attachCompact(msgc.GenericContainer)
|
||||||
|
|
||||||
return PresendMessage{
|
return PresendMessage{
|
||||||
PresendContainer: msgc,
|
PresendContainer: msgc,
|
||||||
|
|
@ -26,19 +29,48 @@ type Message struct {
|
||||||
*message.GenericContainer
|
*message.GenericContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ container.GridMessage = (*Message)(nil)
|
var _ container.MessageRow = (*Message)(nil)
|
||||||
|
|
||||||
func NewMessage(msg cchat.MessageCreate) Message {
|
func NewMessage(msg cchat.MessageCreate) Message {
|
||||||
msgc := message.NewContainer(msg)
|
msgc := message.NewContainer(msg)
|
||||||
|
attachCompact(msgc)
|
||||||
message.FillContainer(msgc, msg)
|
message.FillContainer(msgc, msg)
|
||||||
|
|
||||||
return Message{msgc}
|
return Message{msgc}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmptyMessage() Message {
|
func NewEmptyMessage() Message {
|
||||||
return Message{message.NewEmptyContainer()}
|
ct := message.NewEmptyContainer()
|
||||||
|
attachCompact(ct)
|
||||||
|
|
||||||
|
return Message{ct}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Message) Attach() []gtk.IWidget {
|
var messageTimeCSS = primitives.PrepareClassCSS("message-time", `
|
||||||
return []gtk.IWidget{m.Timestamp, m.Username, m.Content}
|
.message-time {
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
var messageAuthorCSS = primitives.PrepareClassCSS("message-author", `
|
||||||
|
.message-author {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
func attachCompact(container *message.GenericContainer) {
|
||||||
|
container.Timestamp.SetVAlign(gtk.ALIGN_START)
|
||||||
|
container.Username.SetMaxWidthChars(25)
|
||||||
|
container.Username.SetEllipsize(pango.ELLIPSIZE_NONE)
|
||||||
|
container.Username.SetLineWrap(true)
|
||||||
|
container.Username.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||||
|
|
||||||
|
messageTimeCSS(container.Timestamp)
|
||||||
|
messageAuthorCSS(container.Username)
|
||||||
|
|
||||||
|
container.PackStart(container.Timestamp, false, false, 0)
|
||||||
|
container.PackStart(container.Username, false, false, 0)
|
||||||
|
container.PackStart(container.Content, true, true, 0)
|
||||||
|
container.SetClass("compact")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"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/menu"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -12,18 +13,18 @@ import (
|
||||||
// once.
|
// once.
|
||||||
const BacklogLimit = 35
|
const BacklogLimit = 35
|
||||||
|
|
||||||
type GridMessage interface {
|
type MessageRow interface {
|
||||||
message.Container
|
message.Container
|
||||||
// Focusable should return a widget that can be focused.
|
|
||||||
Focusable() gtk.IWidget
|
|
||||||
// Attach should only be called once.
|
// Attach should only be called once.
|
||||||
Attach() []gtk.IWidget
|
Row() *gtk.ListBoxRow
|
||||||
// AttachMenu should override the stored constructor.
|
// AttachMenu should override the stored constructor.
|
||||||
AttachMenu(items []menu.Item) // save memory
|
AttachMenu(items []menu.Item) // save memory
|
||||||
|
// SetReferenceHighlighter sets the reference highlighter into the message.
|
||||||
|
SetReferenceHighlighter(refer labeluri.ReferenceHighlighter)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PresendGridMessage interface {
|
type PresendMessageRow interface {
|
||||||
GridMessage
|
MessageRow
|
||||||
message.PresendContainer
|
message.PresendContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,11 +33,6 @@ type PresendGridMessage interface {
|
||||||
type Container interface {
|
type Container interface {
|
||||||
gtk.IWidget
|
gtk.IWidget
|
||||||
|
|
||||||
// Thread-safe methods.
|
|
||||||
// cchat.MessagesContainer
|
|
||||||
|
|
||||||
// Thread-unsafe methods.
|
|
||||||
|
|
||||||
// Reset resets the message container to its original state.
|
// Reset resets the message container to its original state.
|
||||||
Reset()
|
Reset()
|
||||||
|
|
||||||
|
|
@ -48,13 +44,18 @@ type Container interface {
|
||||||
|
|
||||||
// FirstMessage returns the first message in the buffer. Nil is returned if
|
// FirstMessage returns the first message in the buffer. Nil is returned if
|
||||||
// there's nothing.
|
// there's nothing.
|
||||||
FirstMessage() GridMessage
|
FirstMessage() MessageRow
|
||||||
// TranslateCoordinates is used for scrolling to the message.
|
|
||||||
TranslateCoordinates(parent gtk.IWidget, msg GridMessage) (y int)
|
|
||||||
// AddPresendMessage adds and displays an unsent message.
|
// AddPresendMessage adds and displays an unsent message.
|
||||||
AddPresendMessage(msg input.PresendMessage) PresendGridMessage
|
AddPresendMessage(msg input.PresendMessage) PresendMessageRow
|
||||||
// LatestMessageFrom returns the last message ID with that author.
|
// LatestMessageFrom returns the last message ID with that author.
|
||||||
LatestMessageFrom(authorID string) (msgID string, ok bool)
|
LatestMessageFrom(authorID string) (msgID string, ok bool)
|
||||||
|
// Message finds and returns the message, if any.
|
||||||
|
Message(id cchat.ID, nonce string) MessageRow
|
||||||
|
|
||||||
|
// Highlight temporarily highlights the given message.
|
||||||
|
Highlight(msg MessageRow)
|
||||||
|
// Unhighlight removes the message highlight.
|
||||||
|
Unhighlight()
|
||||||
|
|
||||||
// UI methods.
|
// UI methods.
|
||||||
|
|
||||||
|
|
@ -65,7 +66,7 @@ type Container interface {
|
||||||
// Controller is for menu actions.
|
// Controller is for menu actions.
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
// BindMenu expects the controller to add actioner into the message.
|
// BindMenu expects the controller to add actioner into the message.
|
||||||
BindMenu(GridMessage)
|
BindMenu(MessageRow)
|
||||||
// Bottomed returns whether or not the message scroller is at the bottom.
|
// Bottomed returns whether or not the message scroller is at the bottom.
|
||||||
Bottomed() bool
|
Bottomed() bool
|
||||||
// AuthorEvent is called on message create/update. This is used to update
|
// AuthorEvent is called on message create/update. This is used to update
|
||||||
|
|
@ -74,45 +75,45 @@ type Controller interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor is an interface for making custom message implementations which
|
// Constructor is an interface for making custom message implementations which
|
||||||
// allows GridContainer to generically work with.
|
// allows ListContainer to generically work with.
|
||||||
type Constructor interface {
|
type Constructor interface {
|
||||||
NewMessage(cchat.MessageCreate) GridMessage
|
NewMessage(cchat.MessageCreate) MessageRow
|
||||||
NewPresendMessage(input.PresendMessage) PresendGridMessage
|
NewPresendMessage(input.PresendMessage) PresendMessageRow
|
||||||
}
|
}
|
||||||
|
|
||||||
const ColumnSpacing = 10
|
const ColumnSpacing = 8
|
||||||
|
|
||||||
// GridContainer is an implementation of Container, which allows flexible
|
// ListContainer is an implementation of Container, which allows flexible
|
||||||
// message grids.
|
// message grids.
|
||||||
type GridContainer struct {
|
type ListContainer struct {
|
||||||
*GridStore
|
*ListStore
|
||||||
Controller
|
Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// gridMessage w/ required internals
|
// messageRow w/ required internals
|
||||||
type gridMessage struct {
|
type messageRow struct {
|
||||||
GridMessage
|
MessageRow
|
||||||
presend message.PresendContainer // this shouldn't be here but i'm lazy
|
presend message.PresendContainer // this shouldn't be here but i'm lazy
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Container = (*GridContainer)(nil)
|
var _ Container = (*ListContainer)(nil)
|
||||||
|
|
||||||
func NewGridContainer(constr Constructor, ctrl Controller) *GridContainer {
|
func NewListContainer(constr Constructor, ctrl Controller) *ListContainer {
|
||||||
return &GridContainer{
|
return &ListContainer{
|
||||||
GridStore: NewGridStore(constr, ctrl),
|
ListStore: NewListStore(constr, ctrl),
|
||||||
Controller: ctrl,
|
Controller: ctrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMessageUnsafe inserts a message. It does not clean up old messages.
|
// CreateMessageUnsafe inserts a message. It does not clean up old messages.
|
||||||
func (c *GridContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
func (c *ListContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
// Insert the message first.
|
// Insert the message first.
|
||||||
c.GridStore.CreateMessageUnsafe(msg)
|
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.
|
||||||
func (c *GridContainer) CleanMessages() bool {
|
func (c *ListContainer) CleanMessages() bool {
|
||||||
// Determine if the user is scrolled to the bottom for cleaning up.
|
// Determine if the user is scrolled to the bottom for cleaning up.
|
||||||
if c.Bottomed() {
|
if c.Bottomed() {
|
||||||
// Clean up the backlog.
|
// Clean up the backlog.
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,12 @@ import (
|
||||||
"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/gotk3/gotk3/gtk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Unwrapper provides an interface for messages to be unwrapped. This is used to
|
// Unwrapper provides an interface for messages to be unwrapped. This is used to
|
||||||
// convert between collapsed and full messages.
|
// convert between collapsed and full messages.
|
||||||
type Unwrapper interface {
|
type Unwrapper interface {
|
||||||
Unwrap(grid *gtk.Grid) *message.GenericContainer
|
Unwrap() *message.GenericContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -43,20 +42,20 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
*container.GridContainer
|
*container.ListContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer(ctrl container.Controller) *Container {
|
func NewContainer(ctrl container.Controller) *Container {
|
||||||
c := &Container{}
|
c := &Container{}
|
||||||
c.GridContainer = container.NewGridContainer(c, ctrl)
|
c.ListContainer = container.NewListContainer(c, ctrl)
|
||||||
// A not-so-generous row padding, as we will rely on margins per widget.
|
// A not-so-generous row padding, as we will rely on margins per widget.
|
||||||
c.GridContainer.Grid.SetRowSpacing(4)
|
// c.ListContainer.Grid.SetRowSpacing(4)
|
||||||
|
|
||||||
primitives.AddClass(c, "cozy-container")
|
primitives.AddClass(c, "cozy-container")
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
|
func (c *Container) NewMessage(msg cchat.MessageCreate) container.MessageRow {
|
||||||
// We're not checking for a collapsed message here anymore, as the
|
// We're not checking for a collapsed message here anymore, as the
|
||||||
// CreateMessage method will do that.
|
// CreateMessage method will do that.
|
||||||
|
|
||||||
|
|
@ -79,7 +78,7 @@ func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
|
||||||
return full
|
return full
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
|
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendMessageRow {
|
||||||
// We can do the check here since we're never using NewPresendMessage for
|
// We can do the check here since we're never using NewPresendMessage for
|
||||||
// backlog messages.
|
// backlog messages.
|
||||||
if c.lastMessageIsAuthor(msg.AuthorID(), msg.Author().String(), 0) {
|
if c.lastMessageIsAuthor(msg.AuthorID(), msg.Author().String(), 0) {
|
||||||
|
|
@ -95,9 +94,9 @@ func (c *Container) NewPresendMessage(msg input.PresendMessage) container.Presen
|
||||||
return full
|
return full
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) findAuthorID(authorID string) container.GridMessage {
|
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.GridStore.FindMessage(func(msgc container.GridMessage) bool {
|
return c.ListStore.FindMessage(func(msgc container.MessageRow) bool {
|
||||||
return msgc.AuthorID() == authorID
|
return msgc.AuthorID() == authorID
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -122,11 +121,11 @@ func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) {
|
||||||
|
|
||||||
func (c *Container) lastMessageIsAuthor(id cchat.ID, name string, offset int) bool {
|
func (c *Container) lastMessageIsAuthor(id cchat.ID, name string, offset int) bool {
|
||||||
// Get the offfsetth message from last.
|
// Get the offfsetth message from last.
|
||||||
var last = c.GridStore.NthMessage((c.GridStore.MessagesLen() - 1) + offset)
|
var last = c.ListStore.NthMessage((c.ListStore.MessagesLen() - 1) + offset)
|
||||||
return gridMessageIsAuthor(last, id, name)
|
return gridMessageIsAuthor(last, id, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func gridMessageIsAuthor(gridMsg container.GridMessage, id cchat.ID, name string) bool {
|
func gridMessageIsAuthor(gridMsg container.MessageRow, id cchat.ID, name string) bool {
|
||||||
return gridMsg != nil &&
|
return gridMsg != nil &&
|
||||||
gridMsg.AuthorID() == id &&
|
gridMsg.AuthorID() == id &&
|
||||||
gridMsg.AuthorName() == name
|
gridMsg.AuthorName() == name
|
||||||
|
|
@ -136,11 +135,11 @@ 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.GridContainer.CreateMessageUnsafe(msg)
|
c.ListContainer.CreateMessageUnsafe(msg)
|
||||||
|
|
||||||
// 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.
|
||||||
if c.GridContainer.CleanMessages() {
|
if c.ListContainer.CleanMessages() {
|
||||||
// We need to uncollapse the first (top) message. No length check is
|
// We need to uncollapse the first (top) message. No length check is
|
||||||
// needed here, as we just inserted a message.
|
// needed here, as we just inserted a message.
|
||||||
c.uncompact(c.FirstMessage())
|
c.uncompact(c.FirstMessage())
|
||||||
|
|
@ -149,15 +148,15 @@ func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||||
switch msg.ID() {
|
switch msg.ID() {
|
||||||
// Should we collapse this message? Yes, if the current message is
|
// 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.
|
// inserted at the end and its author is the same as the last author.
|
||||||
case c.GridContainer.LastMessage().ID():
|
case c.ListContainer.LastMessage().ID():
|
||||||
author := msg.Author()
|
author := msg.Author()
|
||||||
if c.lastMessageIsAuthor(author.ID(), author.Name().String(), -1) {
|
if c.lastMessageIsAuthor(author.ID(), author.Name().String(), -1) {
|
||||||
c.compact(c.GridContainer.LastMessage())
|
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.GridContainer.FirstMessage().ID():
|
case c.ListContainer.FirstMessage().ID():
|
||||||
if sec := c.NthMessage(1); sec != nil {
|
if sec := c.NthMessage(1); sec != nil {
|
||||||
// The author is the same; collapse.
|
// The author is the same; collapse.
|
||||||
author := msg.Author()
|
author := msg.Author()
|
||||||
|
|
@ -179,17 +178,17 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
// 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.GridStore.Around(msg.ID())
|
prev, next := c.ListStore.Around(msg.ID())
|
||||||
|
|
||||||
// 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.GridStore.PopMessage(msg.ID())
|
msg := c.ListStore.PopMessage(msg.ID())
|
||||||
|
|
||||||
// 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.
|
||||||
if c.GridStore.MessagesLen() == 0 || prev == nil || next == nil {
|
if c.ListStore.MessagesLen() == 0 || prev == nil || next == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +210,7 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) uncompact(msg container.GridMessage) {
|
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() {
|
if collapse, ok := msg.(Collapsible); !ok || !collapse.Collapsed() {
|
||||||
|
|
@ -225,17 +224,17 @@ func (c *Container) uncompact(msg container.GridMessage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the "lengthy" uncollapse process.
|
// Start the "lengthy" uncollapse process.
|
||||||
full := WrapFullMessage(uw.Unwrap(c.Grid))
|
full := WrapFullMessage(uw.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)
|
c.reuseAvatar(msg.AuthorID(), msg.AvatarURL(), full)
|
||||||
|
|
||||||
// Swap the old next message out for a new one.
|
// Swap the old next message out for a new one.
|
||||||
c.GridStore.SwapMessage(full)
|
c.ListStore.SwapMessage(full)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) compact(msg container.GridMessage) {
|
func (c *Container) compact(msg container.MessageRow) {
|
||||||
// Exit if the message is already collapsed.
|
// Exit if the message is already collapsed.
|
||||||
if collapse, ok := msg.(Collapsible); !ok || collapse.Collapsed() {
|
if collapse, ok := msg.(Collapsible); !ok || collapse.Collapsed() {
|
||||||
return
|
return
|
||||||
|
|
@ -246,8 +245,8 @@ func (c *Container) compact(msg container.GridMessage) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
compact := WrapCollapsedMessage(uw.Unwrap(c.Grid))
|
compact := WrapCollapsedMessage(uw.Unwrap())
|
||||||
message.RefreshContainer(compact, compact.GenericContainer)
|
message.RefreshContainer(compact, compact.GenericContainer)
|
||||||
|
|
||||||
c.GridStore.SwapMessage(compact)
|
c.ListStore.SwapMessage(compact)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,14 @@ func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
|
||||||
gc.Timestamp.SetVAlign(gtk.ALIGN_START)
|
gc.Timestamp.SetVAlign(gtk.ALIGN_START)
|
||||||
gc.Timestamp.SetXAlign(0.5) // middle align
|
gc.Timestamp.SetXAlign(0.5) // middle align
|
||||||
gc.Timestamp.SetMarginStart(container.ColumnSpacing * 2)
|
gc.Timestamp.SetMarginStart(container.ColumnSpacing * 2)
|
||||||
|
gc.Timestamp.SetMarginTop(container.ColumnSpacing)
|
||||||
|
|
||||||
// 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.Username.SetMaxWidthChars(30)
|
gc.PackStart(gc.Timestamp, false, false, 0)
|
||||||
|
gc.PackStart(gc.Content, true, true, 0)
|
||||||
|
gc.SetClass("cozy-collapsed")
|
||||||
|
|
||||||
return &CollapsedMessage{
|
return &CollapsedMessage{
|
||||||
GenericContainer: gc,
|
GenericContainer: gc,
|
||||||
|
|
@ -48,23 +51,15 @@ func (c *CollapsedMessage) UpdateTimestamp(t time.Time) {
|
||||||
c.Timestamp.SetText(humanize.TimeAgoShort(t))
|
c.Timestamp.SetText(humanize.TimeAgoShort(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CollapsedMessage) Unwrap(grid *gtk.Grid) *message.GenericContainer {
|
func (c *CollapsedMessage) Unwrap() *message.GenericContainer {
|
||||||
// Remove GenericContainer's widgets from the containers.
|
// Remove GenericContainer's widgets from the containers.
|
||||||
grid.Remove(c.Timestamp)
|
c.Remove(c.Timestamp)
|
||||||
grid.Remove(c.Content)
|
c.Remove(c.Content)
|
||||||
|
|
||||||
// Return after removing.
|
// Return after removing.
|
||||||
return c.GenericContainer
|
return c.GenericContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CollapsedMessage) Attach() []gtk.IWidget {
|
|
||||||
return []gtk.IWidget{c.Timestamp, c.Content}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CollapsedMessage) Focusable() gtk.IWidget {
|
|
||||||
return c.Timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
type CollapsedSendingMessage struct {
|
type CollapsedSendingMessage struct {
|
||||||
*CollapsedMessage
|
*CollapsedMessage
|
||||||
message.PresendContainer
|
message.PresendContainer
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ type AvatarPixbufCopier interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ AvatarPixbufCopier = (*FullMessage)(nil)
|
_ AvatarPixbufCopier = (*FullMessage)(nil)
|
||||||
_ message.Container = (*FullMessage)(nil)
|
_ message.Container = (*FullMessage)(nil)
|
||||||
_ container.GridMessage = (*FullMessage)(nil)
|
_ container.MessageRow = (*FullMessage)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
var boldCSS = primitives.PrepareCSS(`
|
var boldCSS = primitives.PrepareCSS(`
|
||||||
|
|
@ -64,14 +64,14 @@ func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
|
||||||
|
|
||||||
func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
||||||
avatar := NewAvatar()
|
avatar := NewAvatar()
|
||||||
avatar.SetMarginTop(TopFullMargin)
|
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 := gc.Username.Output(); len(output.Mentions) > 0 {
|
||||||
labeluri.PopoverMentioner(w, output.Input, output.Mentions[0])
|
labeluri.PopoverMentioner(w, output.Input, output.Mentions[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// We don't call avatar.Show(). That's called in Attach.
|
avatar.Show()
|
||||||
|
|
||||||
// Style the timestamp accordingly.
|
// Style the timestamp accordingly.
|
||||||
gc.Timestamp.SetXAlign(0.0) // left-align
|
gc.Timestamp.SetXAlign(0.0) // left-align
|
||||||
|
|
@ -96,11 +96,16 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
||||||
main.PackStart(gc.Content, false, false, 0)
|
main.PackStart(gc.Content, false, false, 0)
|
||||||
main.SetMarginTop(TopFullMargin)
|
main.SetMarginTop(TopFullMargin)
|
||||||
main.SetMarginEnd(container.ColumnSpacing * 2)
|
main.SetMarginEnd(container.ColumnSpacing * 2)
|
||||||
|
main.SetMarginStart(container.ColumnSpacing)
|
||||||
main.Show()
|
main.Show()
|
||||||
|
|
||||||
// Also attach a class for the main box shown on the right.
|
// Also attach a class for the main box shown on the right.
|
||||||
primitives.AddClass(main, "cozy-main")
|
primitives.AddClass(main, "cozy-main")
|
||||||
|
|
||||||
|
gc.PackStart(avatar, false, false, 0)
|
||||||
|
gc.PackStart(main, true, true, 0)
|
||||||
|
gc.SetClass("cozy-full")
|
||||||
|
|
||||||
return &FullMessage{
|
return &FullMessage{
|
||||||
GenericContainer: gc,
|
GenericContainer: gc,
|
||||||
Avatar: avatar,
|
Avatar: avatar,
|
||||||
|
|
@ -111,7 +116,7 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
||||||
|
|
||||||
func (m *FullMessage) Collapsed() bool { return false }
|
func (m *FullMessage) Collapsed() bool { return false }
|
||||||
|
|
||||||
func (m *FullMessage) Unwrap(grid *gtk.Grid) *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.HeaderBox.Remove(m.Username)
|
||||||
m.HeaderBox.Remove(m.Timestamp)
|
m.HeaderBox.Remove(m.Timestamp)
|
||||||
|
|
@ -122,22 +127,13 @@ func (m *FullMessage) Unwrap(grid *gtk.Grid) *message.GenericContainer {
|
||||||
m.Avatar.Hide()
|
m.Avatar.Hide()
|
||||||
|
|
||||||
// Remove the message from the grid.
|
// Remove the message from the grid.
|
||||||
grid.Remove(m.Avatar)
|
m.Remove(m.Avatar)
|
||||||
grid.Remove(m.MainBox)
|
m.Remove(m.MainBox)
|
||||||
|
|
||||||
// Return after removing.
|
// Return after removing.
|
||||||
return m.GenericContainer
|
return m.GenericContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FullMessage) Attach() []gtk.IWidget {
|
|
||||||
m.Avatar.Show()
|
|
||||||
return []gtk.IWidget{m.Avatar, m.MainBox}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FullMessage) Focusable() gtk.IWidget {
|
|
||||||
return m.Avatar
|
|
||||||
}
|
|
||||||
|
|
||||||
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.SetText(humanize.TimeAgoLong(t))
|
||||||
|
|
@ -177,9 +173,8 @@ type FullSendingMessage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// _ AvatarPixbufCopier = (*FullSendingMessage)(nil)
|
_ message.Container = (*FullSendingMessage)(nil)
|
||||||
_ message.Container = (*FullSendingMessage)(nil)
|
_ container.MessageRow = (*FullSendingMessage)(nil)
|
||||||
_ container.GridMessage = (*FullSendingMessage)(nil)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage {
|
func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@ package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"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/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type messageKey struct {
|
type messageKey struct {
|
||||||
|
|
@ -19,57 +20,51 @@ type messageKey struct {
|
||||||
func nonceKey(nonce string) messageKey { return messageKey{nonce, true} }
|
func nonceKey(nonce string) messageKey { return messageKey{nonce, true} }
|
||||||
func idKey(id cchat.ID) messageKey { return messageKey{id, false} }
|
func idKey(id cchat.ID) messageKey { return messageKey{id, false} }
|
||||||
|
|
||||||
type GridStore struct {
|
var messageListCSS = primitives.PrepareClassCSS("message-list", `
|
||||||
*gtk.Grid
|
.message-list { background: transparent; }
|
||||||
|
`)
|
||||||
|
|
||||||
|
type ListStore struct {
|
||||||
|
*gtk.ListBox
|
||||||
|
|
||||||
Construct Constructor
|
Construct Constructor
|
||||||
Controller Controller
|
Controller Controller
|
||||||
|
|
||||||
resetMe bool
|
resetMe bool
|
||||||
|
|
||||||
messages map[messageKey]*gridMessage
|
messages map[messageKey]*messageRow
|
||||||
messageList *list.List
|
messageList *list.List
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGridStore(constr Constructor, ctrl Controller) *GridStore {
|
func NewListStore(constr Constructor, ctrl Controller) *ListStore {
|
||||||
grid, _ := gtk.GridNew()
|
listBox, _ := gtk.ListBoxNew()
|
||||||
grid.SetColumnSpacing(ColumnSpacing)
|
listBox.SetSelectionMode(gtk.SELECTION_NONE)
|
||||||
grid.SetRowSpacing(5)
|
listBox.Show()
|
||||||
grid.SetMarginStart(5)
|
messageListCSS(listBox)
|
||||||
grid.SetMarginEnd(5)
|
|
||||||
grid.Show()
|
|
||||||
|
|
||||||
primitives.AddClass(grid, "message-grid")
|
return &ListStore{
|
||||||
|
ListBox: listBox,
|
||||||
return &GridStore{
|
|
||||||
Grid: grid,
|
|
||||||
Construct: constr,
|
Construct: constr,
|
||||||
Controller: ctrl,
|
Controller: ctrl,
|
||||||
messages: make(map[messageKey]*gridMessage, BacklogLimit+1),
|
messages: make(map[messageKey]*messageRow, BacklogLimit+1),
|
||||||
messageList: list.New(),
|
messageList: list.New(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) Reset() {
|
func (c *ListStore) Reset() {
|
||||||
primitives.RemoveChildren(c.Grid)
|
primitives.RemoveChildren(c.ListBox)
|
||||||
c.messages = make(map[messageKey]*gridMessage, BacklogLimit+1)
|
c.messages = make(map[messageKey]*messageRow, BacklogLimit+1)
|
||||||
c.messageList = list.New()
|
c.messageList = list.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) MessagesLen() int {
|
func (c *ListStore) MessagesLen() int {
|
||||||
return c.messageList.Len()
|
return c.messageList.Len()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) attachGrid(row int, widgets []gtk.IWidget) {
|
func (c *ListStore) findElement(id cchat.ID) (*list.Element, *messageRow, int) {
|
||||||
for i, w := range widgets {
|
|
||||||
c.Grid.Attach(w, i, row, 1, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GridStore) findElement(id cchat.ID) (*list.Element, *gridMessage, int) {
|
|
||||||
var index = c.messageList.Len() - 1
|
var index = c.messageList.Len() - 1
|
||||||
for elem := c.messageList.Back(); elem != nil; elem = elem.Prev() {
|
for elem := c.messageList.Back(); elem != nil; elem = elem.Prev() {
|
||||||
if gridMsg := elem.Value.(*gridMessage); gridMsg.ID() == id {
|
if gridMsg := elem.Value.(*messageRow); gridMsg.ID() == id {
|
||||||
return elem, gridMsg, index
|
return elem, gridMsg, index
|
||||||
}
|
}
|
||||||
index--
|
index--
|
||||||
|
|
@ -78,89 +73,62 @@ func (c *GridStore) findElement(id cchat.ID) (*list.Element, *gridMessage, int)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findIndex searches backwards for id.
|
// findIndex searches backwards for id.
|
||||||
func (c *GridStore) findIndex(id cchat.ID) (*gridMessage, int) {
|
func (c *ListStore) findIndex(id cchat.ID) (*messageRow, int) {
|
||||||
_, gridMsg, ix := c.findElement(id)
|
_, gridMsg, ix := c.findElement(id)
|
||||||
return gridMsg, ix
|
return gridMsg, ix
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoordinateTranslator interface {
|
|
||||||
TranslateCoordinates(dest gtk.IWidget, srcX int, srcY int) (destX int, destY int, e error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ CoordinateTranslator = (*gtk.Widget)(nil)
|
|
||||||
|
|
||||||
func (c *GridStore) TranslateCoordinates(parent gtk.IWidget, msg GridMessage) (y int) {
|
|
||||||
m, i := c.findIndex(msg.ID())
|
|
||||||
if i < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
w, _ := m.Focusable().(CoordinateTranslator)
|
|
||||||
|
|
||||||
// x is not needed.
|
|
||||||
_, y, err := w.TranslateCoordinates(parent, 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(errors.Wrap(err, "Failed to translate coords while focusing"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap changes the message with the ID to the given message. This provides a
|
// Swap changes the message with the ID to the given message. This provides a
|
||||||
// low level API for edits that need a new Attach method.
|
// low level API for edits that need a new Attach method.
|
||||||
//
|
//
|
||||||
// TODO: combine compact and full so they share the same attach method.
|
// TODO: combine compact and full so they share the same attach method.
|
||||||
func (c *GridStore) SwapMessage(msg GridMessage) bool {
|
func (c *ListStore) SwapMessage(msg MessageRow) bool {
|
||||||
// Wrap msg inside a *gridMessage if it's not already.
|
// Wrap msg inside a *messageRow if it's not already.
|
||||||
m, ok := msg.(*gridMessage)
|
m, ok := msg.(*messageRow)
|
||||||
if !ok {
|
if !ok {
|
||||||
m = &gridMessage{GridMessage: msg}
|
m = &messageRow{MessageRow: msg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current message's index.
|
// Get the current message's index.
|
||||||
_, ix := c.findIndex(msg.ID())
|
oldMsg, ix := c.findIndex(msg.ID())
|
||||||
if ix == -1 {
|
if ix == -1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a row at index. The actual row we want to delete will be shifted
|
// Add a row at index. The actual row we want to delete will be shifted
|
||||||
// downwards.
|
// downwards.
|
||||||
c.Grid.InsertRow(ix)
|
c.ListBox.Insert(m.Row(), ix)
|
||||||
|
|
||||||
// Delete the to-be-replaced message, which we have shifted downwards
|
// Delete the to-be-replaced message.
|
||||||
// earlier, so we add 1.
|
oldMsg.Row().Destroy()
|
||||||
c.Grid.RemoveRow(ix + 1)
|
|
||||||
|
|
||||||
// Let the new message be attached on top of the to-be-replaced message.
|
|
||||||
c.attachGrid(ix, m.Attach())
|
|
||||||
|
|
||||||
// Set the message into the map.
|
// Set the message into the map.
|
||||||
c.messages[idKey(m.ID())] = m
|
row := c.messages[idKey(m.ID())]
|
||||||
|
*row = *m
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Around returns the message before and after the given ID, or nil if none.
|
// Around returns the message before and after the given ID, or nil if none.
|
||||||
func (c *GridStore) Around(id cchat.ID) (before, after GridMessage) {
|
func (c *ListStore) Around(id cchat.ID) (before, after MessageRow) {
|
||||||
gridBefore, gridAfter := c.around(id)
|
gridBefore, gridAfter := c.around(id)
|
||||||
|
|
||||||
if gridBefore != nil {
|
if gridBefore != nil {
|
||||||
before = gridBefore.GridMessage
|
before = gridBefore.MessageRow
|
||||||
}
|
}
|
||||||
if gridAfter != nil {
|
if gridAfter != nil {
|
||||||
after = gridAfter.GridMessage
|
after = gridAfter.MessageRow
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) around(id cchat.ID) (before, after *gridMessage) {
|
func (c *ListStore) around(id cchat.ID) (before, after *messageRow) {
|
||||||
var last *gridMessage
|
var last *messageRow
|
||||||
var next bool
|
var next bool
|
||||||
|
|
||||||
for elem := c.messageList.Front(); elem != nil; elem = elem.Next() {
|
for elem := c.messageList.Front(); elem != nil; elem = elem.Next() {
|
||||||
message := elem.Value.(*gridMessage)
|
message := elem.Value.(*messageRow)
|
||||||
if next {
|
if next {
|
||||||
after = message
|
after = message
|
||||||
break
|
break
|
||||||
|
|
@ -179,9 +147,9 @@ func (c *GridStore) around(id cchat.ID) (before, after *gridMessage) {
|
||||||
|
|
||||||
// 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 *GridStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
func (c *ListStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
||||||
// FindMessage already looks from the latest messages.
|
// FindMessage already looks from the latest messages.
|
||||||
var msg = c.FindMessage(func(msg GridMessage) bool {
|
var msg = c.FindMessage(func(msg MessageRow) bool {
|
||||||
return msg.AuthorID() == userID
|
return msg.AuthorID() == userID
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -194,14 +162,14 @@ func (c *GridStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
||||||
|
|
||||||
// FindMessage iterates backwards and returns the message if isMessage() returns
|
// FindMessage iterates backwards and returns the message if isMessage() returns
|
||||||
// true on that message.
|
// true on that message.
|
||||||
func (c *GridStore) FindMessage(isMessage func(msg GridMessage) bool) GridMessage {
|
func (c *ListStore) FindMessage(isMessage func(msg MessageRow) bool) MessageRow {
|
||||||
for elem := c.messageList.Back(); elem != nil; elem = elem.Prev() {
|
for elem := c.messageList.Back(); elem != nil; elem = elem.Prev() {
|
||||||
gridMsg := elem.Value.(*gridMessage)
|
gridMsg := elem.Value.(*messageRow)
|
||||||
// Ignore sending messages.
|
// Ignore sending messages.
|
||||||
if gridMsg.presend != nil {
|
if gridMsg.presend != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if gridMsg := gridMsg.GridMessage; isMessage(gridMsg) {
|
if gridMsg := gridMsg.MessageRow; isMessage(gridMsg) {
|
||||||
return gridMsg
|
return gridMsg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,11 +178,11 @@ func (c *GridStore) FindMessage(isMessage func(msg GridMessage) bool) GridMessag
|
||||||
}
|
}
|
||||||
|
|
||||||
// NthMessage returns the nth message.
|
// NthMessage returns the nth message.
|
||||||
func (c *GridStore) NthMessage(n int) GridMessage {
|
func (c *ListStore) NthMessage(n int) MessageRow {
|
||||||
var index = 0
|
var index = 0
|
||||||
for elem := c.messageList.Front(); elem != nil; elem = elem.Next() {
|
for elem := c.messageList.Front(); elem != nil; elem = elem.Next() {
|
||||||
if index == n {
|
if index == n {
|
||||||
return elem.Value.(*gridMessage).GridMessage
|
return elem.Value.(*messageRow).MessageRow
|
||||||
}
|
}
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
|
|
@ -223,33 +191,33 @@ func (c *GridStore) NthMessage(n int) GridMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FirstMessage returns the first message.
|
// FirstMessage returns the first message.
|
||||||
func (c *GridStore) FirstMessage() GridMessage {
|
func (c *ListStore) FirstMessage() MessageRow {
|
||||||
if c.messageList.Len() == 0 {
|
if c.messageList.Len() == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Long unwrap.
|
// Long unwrap.
|
||||||
return c.messageList.Front().Value.(*gridMessage).GridMessage
|
return c.messageList.Front().Value.(*messageRow).MessageRow
|
||||||
}
|
}
|
||||||
|
|
||||||
// LastMessage returns the latest message.
|
// LastMessage returns the latest message.
|
||||||
func (c *GridStore) LastMessage() GridMessage {
|
func (c *ListStore) LastMessage() MessageRow {
|
||||||
if c.messageList.Len() == 0 {
|
if c.messageList.Len() == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Long unwrap.
|
// Long unwrap.
|
||||||
return c.messageList.Back().Value.(*gridMessage).GridMessage
|
return c.messageList.Back().Value.(*messageRow).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 *GridStore) Message(msgID cchat.ID, nonce string) GridMessage {
|
func (c *ListStore) Message(msgID cchat.ID, nonce string) MessageRow {
|
||||||
if m := c.message(msgID, nonce); m != nil {
|
if m := c.message(msgID, nonce); m != nil {
|
||||||
return m.GridMessage
|
return m.MessageRow
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) message(msgID cchat.ID, nonce string) *gridMessage {
|
func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
|
||||||
// Search using the ID first.
|
// Search using the ID first.
|
||||||
m, ok := c.messages[idKey(msgID)]
|
m, ok := c.messages[idKey(msgID)]
|
||||||
if ok {
|
if ok {
|
||||||
|
|
@ -279,16 +247,16 @@ func (c *GridStore) message(msgID cchat.ID, nonce string) *gridMessage {
|
||||||
|
|
||||||
// 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 *GridStore) AddPresendMessage(msg input.PresendMessage) PresendGridMessage {
|
func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRow {
|
||||||
presend := c.Construct.NewPresendMessage(msg)
|
presend := c.Construct.NewPresendMessage(msg)
|
||||||
|
|
||||||
msgc := &gridMessage{
|
msgc := &messageRow{
|
||||||
GridMessage: presend,
|
MessageRow: presend,
|
||||||
presend: presend,
|
presend: presend,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the message into the grid.
|
// Set the message into the list.
|
||||||
c.attachGrid(c.MessagesLen(), msgc.Attach())
|
c.ListBox.Insert(msgc.Row(), c.MessagesLen())
|
||||||
// Append the message.
|
// Append the message.
|
||||||
c.messageList.PushBack(msgc)
|
c.messageList.PushBack(msgc)
|
||||||
// Set the NONCE into the message map.
|
// Set the NONCE into the message map.
|
||||||
|
|
@ -297,26 +265,31 @@ func (c *GridStore) AddPresendMessage(msg input.PresendMessage) PresendGridMessa
|
||||||
return presend
|
return presend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ListStore) bindMessage(msgc *messageRow) {
|
||||||
|
msgc.SetReferenceHighlighter(c)
|
||||||
|
c.Controller.BindMenu(msgc.MessageRow)
|
||||||
|
}
|
||||||
|
|
||||||
// Many attempts were made to have CreateMessageUnsafe return an index. That is
|
// Many attempts were made to have CreateMessageUnsafe return an index. That is
|
||||||
// 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 *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
func (c *ListStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
// Call the event handler last.
|
// Call the event handler last.
|
||||||
defer c.Controller.AuthorEvent(msg.Author())
|
defer c.Controller.AuthorEvent(msg.Author())
|
||||||
|
|
||||||
// Attempt to update before insertion (aka upsert).
|
// Do not attempt to update before insertion (aka upsert).
|
||||||
if msgc := c.message(msg.ID(), msg.Nonce()); msgc != nil {
|
if msgc := c.message(msg.ID(), msg.Nonce()); msgc != nil {
|
||||||
msgc.UpdateAuthor(msg.Author())
|
msgc.UpdateAuthor(msg.Author())
|
||||||
msgc.UpdateContent(msg.Content(), false)
|
msgc.UpdateContent(msg.Content(), false)
|
||||||
msgc.UpdateTimestamp(msg.Time())
|
msgc.UpdateTimestamp(msg.Time())
|
||||||
|
|
||||||
c.Controller.BindMenu(msgc.GridMessage)
|
c.bindMessage(msgc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msgc := &gridMessage{
|
msgc := &messageRow{
|
||||||
GridMessage: c.Construct.NewMessage(msg),
|
MessageRow: c.Construct.NewMessage(msg),
|
||||||
}
|
}
|
||||||
msgTime := msg.Time()
|
msgTime := msg.Time()
|
||||||
|
|
||||||
|
|
@ -325,7 +298,7 @@ func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
|
|
||||||
// Iterate and compare timestamp to find where to insert a message.
|
// Iterate and compare timestamp to find where to insert a message.
|
||||||
for after != nil {
|
for after != nil {
|
||||||
if msgTime.After(after.Value.(*gridMessage).Time()) {
|
if msgTime.After(after.Value.(*messageRow).Time()) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
index--
|
index--
|
||||||
|
|
@ -343,16 +316,15 @@ func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the message into the grid.
|
// Set the message into the grid.
|
||||||
c.Grid.InsertRow(index)
|
c.ListBox.Insert(msgc.Row(), index)
|
||||||
c.attachGrid(index, msgc.Attach())
|
|
||||||
|
|
||||||
// Set the NONCE into the message map.
|
// Set the ID into the message map.
|
||||||
c.messages[nonceKey(msgc.Nonce())] = msgc
|
c.messages[idKey(msgc.ID())] = msgc
|
||||||
|
|
||||||
c.Controller.BindMenu(msgc)
|
c.bindMessage(msgc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
|
func (c *ListStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
|
||||||
// Call the event handler last.
|
// Call the event handler last.
|
||||||
defer c.Controller.AuthorEvent(msg.Author())
|
defer c.Controller.AuthorEvent(msg.Author())
|
||||||
|
|
||||||
|
|
@ -368,21 +340,21 @@ func (c *GridStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) DeleteMessageUnsafe(msg cchat.MessageDelete) {
|
func (c *ListStore) DeleteMessageUnsafe(msg cchat.MessageDelete) {
|
||||||
c.PopMessage(msg.ID())
|
c.PopMessage(msg.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
// PopMessage deletes a message off of the list and return the deleted message.
|
// PopMessage deletes a message off of the list and return the deleted message.
|
||||||
func (c *GridStore) PopMessage(id cchat.ID) (msg GridMessage) {
|
func (c *ListStore) PopMessage(id cchat.ID) (msg MessageRow) {
|
||||||
// Get the raw element to delete it off the list.
|
// Get the raw element to delete it off the list.
|
||||||
elem, gridMsg, ix := c.findElement(id)
|
elem, gridMsg, _ := c.findElement(id)
|
||||||
if elem == nil {
|
if elem == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
msg = gridMsg.GridMessage
|
msg = gridMsg.MessageRow
|
||||||
|
|
||||||
// Remove off of the Gtk grid.
|
// Remove off of the Gtk grid.
|
||||||
c.Grid.RemoveRow(ix)
|
gridMsg.Row().Destroy()
|
||||||
// Pop off the slice.
|
// Pop off the slice.
|
||||||
c.messageList.Remove(elem)
|
c.messageList.Remove(elem)
|
||||||
// Delete off the map.
|
// Delete off the map.
|
||||||
|
|
@ -393,7 +365,7 @@ func (c *GridStore) PopMessage(id cchat.ID) (msg GridMessage) {
|
||||||
|
|
||||||
// DeleteEarliest deletes the n earliest messages. It does nothing if n is or
|
// DeleteEarliest deletes the n earliest messages. It does nothing if n is or
|
||||||
// less than 0.
|
// less than 0.
|
||||||
func (c *GridStore) DeleteEarliest(n int) {
|
func (c *ListStore) DeleteEarliest(n int) {
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -401,7 +373,7 @@ func (c *GridStore) DeleteEarliest(n int) {
|
||||||
// Since container/list nils out the next element, we can't just call Next
|
// Since container/list nils out the next element, we can't just call Next
|
||||||
// after deleting, so we have to call Next manually before Removing.
|
// after deleting, so we have to call Next manually before Removing.
|
||||||
for elem := c.messageList.Front(); elem != nil && n != 0; n-- {
|
for elem := c.messageList.Front(); elem != nil && n != 0; n-- {
|
||||||
gridMsg := elem.Value.(*gridMessage)
|
gridMsg := elem.Value.(*messageRow)
|
||||||
|
|
||||||
if id := gridMsg.ID(); id != "" {
|
if id := gridMsg.ID(); id != "" {
|
||||||
delete(c.messages, idKey(id))
|
delete(c.messages, idKey(id))
|
||||||
|
|
@ -410,10 +382,30 @@ func (c *GridStore) DeleteEarliest(n int) {
|
||||||
delete(c.messages, nonceKey(nonce))
|
delete(c.messages, nonceKey(nonce))
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Grid.RemoveRow(0)
|
gridMsg.Row().Destroy()
|
||||||
|
|
||||||
next := elem.Next()
|
next := elem.Next()
|
||||||
c.messageList.Remove(elem)
|
c.messageList.Remove(elem)
|
||||||
elem = next
|
elem = next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ListStore) HighlightReference(ref markup.ReferenceSegment) {
|
||||||
|
msg := c.message(ref.MessageID(), "")
|
||||||
|
log.Println("Highlighting", ref.MessageID())
|
||||||
|
if msg != nil {
|
||||||
|
c.Highlight(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ListStore) Highlight(msg MessageRow) {
|
||||||
|
gts.ExecLater(func() {
|
||||||
|
row := msg.Row()
|
||||||
|
row.GrabFocus()
|
||||||
|
c.ListBox.DragHighlightRow(row)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ListStore) Unhighlight() {
|
||||||
|
c.ListBox.DragUnhighlightRow()
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,14 @@ import (
|
||||||
// Controller is an interface to control message containers.
|
// Controller is an interface to control message containers.
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
AddPresendMessage(msg PresendMessage) (onErr func(error))
|
AddPresendMessage(msg PresendMessage) (onErr func(error))
|
||||||
LatestMessageFrom(userID string) (messageID string, ok bool)
|
LatestMessageFrom(userID cchat.ID) (messageID cchat.ID, ok bool)
|
||||||
|
MessageAuthorMarkup(msgID cchat.ID) (markup string, ok bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LabelBorrower is an interface that allows the caller to borrow a label.
|
||||||
|
type LabelBorrower interface {
|
||||||
|
BorrowLabel(markup string)
|
||||||
|
Unborrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputView struct {
|
type InputView struct {
|
||||||
|
|
@ -53,7 +60,7 @@ var inputBoxCSS = primitives.PrepareClassCSS("input-box", `
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
func NewView(ctrl Controller) *InputView {
|
func NewView(ctrl Controller, labeler LabelBorrower) *InputView {
|
||||||
text, _ := gtk.TextViewNew()
|
text, _ := gtk.TextViewNew()
|
||||||
text.SetSensitive(false)
|
text.SetSensitive(false)
|
||||||
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||||
|
|
@ -72,7 +79,7 @@ func NewView(ctrl Controller) *InputView {
|
||||||
c := completion.NewCompleter(text)
|
c := completion.NewCompleter(text)
|
||||||
|
|
||||||
// Bind the input callback later.
|
// Bind the input callback later.
|
||||||
f := NewField(text, ctrl)
|
f := NewField(text, ctrl, labeler)
|
||||||
f.Show()
|
f.Show()
|
||||||
|
|
||||||
return &InputView{f, c}
|
return &InputView{f, c}
|
||||||
|
|
@ -99,6 +106,13 @@ func (v *InputView) SetMessenger(session cchat.Session, messenger cchat.Messenge
|
||||||
// wrapSpellCheck is a no-op but is replaced by gspell in ./spellcheck.go.
|
// wrapSpellCheck is a no-op but is replaced by gspell in ./spellcheck.go.
|
||||||
var wrapSpellCheck = func(textView *gtk.TextView) {}
|
var wrapSpellCheck = func(textView *gtk.TextView) {}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sendButtonIcon = "mail-send-symbolic"
|
||||||
|
editButtonIcon = "document-edit-symbolic"
|
||||||
|
replyButtonIcon = "mail-reply-sender-symbolic"
|
||||||
|
sendButtonSize = gtk.ICON_SIZE_BUTTON
|
||||||
|
)
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
// Box contains the field box and the attachment container.
|
// Box contains the field box and the attachment container.
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
|
|
@ -113,10 +127,13 @@ type Field struct {
|
||||||
text *gtk.TextView // const
|
text *gtk.TextView // const
|
||||||
buffer *gtk.TextBuffer // const
|
buffer *gtk.TextBuffer // const
|
||||||
|
|
||||||
send *gtk.Button
|
sendIcon *gtk.Image
|
||||||
|
send *gtk.Button
|
||||||
|
|
||||||
attach *gtk.Button
|
attach *gtk.Button
|
||||||
|
|
||||||
ctrl Controller
|
ctrl Controller
|
||||||
|
indicator LabelBorrower
|
||||||
|
|
||||||
// Embed a state field which allows us to easily reset it.
|
// Embed a state field which allows us to easily reset it.
|
||||||
fieldState
|
fieldState
|
||||||
|
|
@ -130,7 +147,9 @@ type fieldState struct {
|
||||||
editor cchat.Editor
|
editor cchat.Editor
|
||||||
typing cchat.TypingIndicator
|
typing cchat.TypingIndicator
|
||||||
|
|
||||||
editingID string // never empty
|
replyingID cchat.ID
|
||||||
|
editingID cchat.ID
|
||||||
|
|
||||||
lastTyped time.Time
|
lastTyped time.Time
|
||||||
typerDura time.Duration
|
typerDura time.Duration
|
||||||
}
|
}
|
||||||
|
|
@ -152,8 +171,12 @@ var scrolledInputCSS = primitives.PrepareClassCSS("scrolled-input", `
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
func NewField(text *gtk.TextView, ctrl Controller, labeler LabelBorrower) *Field {
|
||||||
field := &Field{text: text, ctrl: ctrl}
|
field := &Field{
|
||||||
|
text: text,
|
||||||
|
ctrl: ctrl,
|
||||||
|
indicator: labeler,
|
||||||
|
}
|
||||||
field.buffer, _ = text.GetBuffer()
|
field.buffer, _ = text.GetBuffer()
|
||||||
|
|
||||||
field.Username = username.NewContainer()
|
field.Username = username.NewContainer()
|
||||||
|
|
@ -163,13 +186,17 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||||
field.TextScroll.Show()
|
field.TextScroll.Show()
|
||||||
scrolledInputCSS(field.TextScroll)
|
scrolledInputCSS(field.TextScroll)
|
||||||
|
|
||||||
field.attach, _ = gtk.ButtonNewFromIconName("mail-attachment-symbolic", gtk.ICON_SIZE_BUTTON)
|
field.attach, _ = gtk.ButtonNewFromIconName("mail-attachment-symbolic", sendButtonSize)
|
||||||
field.attach.SetRelief(gtk.RELIEF_NONE)
|
field.attach.SetRelief(gtk.RELIEF_NONE)
|
||||||
field.attach.SetSensitive(false)
|
field.attach.SetSensitive(false)
|
||||||
// Only show this if the server supports it (upload == true).
|
// Only show this if the server supports it (upload == true).
|
||||||
primitives.AddClass(field.attach, "attach-button")
|
primitives.AddClass(field.attach, "attach-button")
|
||||||
|
|
||||||
field.send, _ = gtk.ButtonNewFromIconName("mail-send-symbolic", gtk.ICON_SIZE_BUTTON)
|
field.sendIcon, _ = gtk.ImageNewFromIconName(sendButtonIcon, sendButtonSize)
|
||||||
|
field.sendIcon.Show()
|
||||||
|
|
||||||
|
field.send, _ = gtk.ButtonNew()
|
||||||
|
field.send.SetImage(field.sendIcon)
|
||||||
field.send.SetRelief(gtk.RELIEF_NONE)
|
field.send.SetRelief(gtk.RELIEF_NONE)
|
||||||
field.send.Show()
|
field.send.Show()
|
||||||
primitives.AddClass(field.send, "send-button")
|
primitives.AddClass(field.send, "send-button")
|
||||||
|
|
@ -278,12 +305,27 @@ func (f *Field) SetAllowUpload(allow bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Field) StartReplyingTo(msgID cchat.ID) {
|
||||||
|
// Clear the input to prevent mixing.
|
||||||
|
f.clearText()
|
||||||
|
|
||||||
|
f.replyingID = msgID
|
||||||
|
f.sendIcon.SetFromIconName(replyButtonIcon, gtk.ICON_SIZE_BUTTON)
|
||||||
|
|
||||||
|
name, ok := f.ctrl.MessageAuthorMarkup(msgID)
|
||||||
|
if !ok {
|
||||||
|
name = "message"
|
||||||
|
}
|
||||||
|
|
||||||
|
f.indicator.BorrowLabel("Replying to " + name)
|
||||||
|
}
|
||||||
|
|
||||||
// Editable returns whether or not the input field can be edited.
|
// Editable returns whether or not the input field can be edited.
|
||||||
func (f *Field) Editable(msgID string) bool {
|
func (f *Field) Editable(msgID cchat.ID) bool {
|
||||||
return f.editor != nil && f.editor.IsEditable(msgID)
|
return f.editor != nil && f.editor.IsEditable(msgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) StartEditing(msgID string) bool {
|
func (f *Field) StartEditing(msgID cchat.ID) bool {
|
||||||
// Do we support message editing? If not, exit.
|
// Do we support message editing? If not, exit.
|
||||||
if !f.Editable(msgID) {
|
if !f.Editable(msgID) {
|
||||||
return false
|
return false
|
||||||
|
|
@ -297,11 +339,18 @@ func (f *Field) StartEditing(msgID string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear the input before editing to prevent mixing replying and editing
|
||||||
|
// together.
|
||||||
|
f.clearText()
|
||||||
|
|
||||||
// Set the current editing state and set the input after requesting the
|
// Set the current editing state and set the input after requesting the
|
||||||
// content.
|
// content.
|
||||||
f.editingID = msgID
|
f.editingID = msgID
|
||||||
f.buffer.SetText(content)
|
f.buffer.SetText(content)
|
||||||
|
|
||||||
|
f.indicator.BorrowLabel("Editing Message")
|
||||||
|
f.sendIcon.SetFromIconName(editButtonIcon, sendButtonSize)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,15 +361,17 @@ func (f *Field) StopEditing() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
f.editingID = ""
|
|
||||||
f.clearText()
|
f.clearText()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearText resets the input field
|
// clearText resets the input field
|
||||||
func (f *Field) clearText() {
|
func (f *Field) clearText() {
|
||||||
|
f.editingID = ""
|
||||||
|
f.replyingID = ""
|
||||||
f.buffer.Delete(f.buffer.GetBounds())
|
f.buffer.Delete(f.buffer.GetBounds())
|
||||||
|
f.sendIcon.SetFromIconName(sendButtonIcon, sendButtonSize)
|
||||||
|
f.indicator.Unborrow()
|
||||||
f.Attachments.Reset()
|
f.Attachments.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,14 +67,9 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
|
||||||
// Take the event.
|
// Take the event.
|
||||||
return true
|
return true
|
||||||
|
|
||||||
// There are multiple things to do here when we press the Escape key.
|
// Clear text when the Escape key is pressed.
|
||||||
case key == gdk.KEY_Escape:
|
case key == gdk.KEY_Escape:
|
||||||
// First, we'd want to cancel editing if we have one.
|
f.clearText()
|
||||||
if f.editingID != "" {
|
|
||||||
return f.StopEditing() // always returns true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second... Nothing yet?
|
|
||||||
|
|
||||||
// Ctrl+V is paste.
|
// Ctrl+V is paste.
|
||||||
case key == gdk.KEY_v && bithas(mask, cntrlMask):
|
case key == gdk.KEY_v && bithas(mask, cntrlMask):
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ func (f *Field) sendInput() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the input text.
|
// Get the input text and the reply ID.
|
||||||
var text = f.getText()
|
text := f.getText()
|
||||||
|
|
||||||
// Are we editing anything?
|
// Are we editing anything?
|
||||||
if id := f.editingID; f.Editable(id) && id != "" {
|
if id := f.editingID; f.Editable(id) && id != "" {
|
||||||
|
|
@ -70,6 +70,7 @@ func (f *Field) sendInput() {
|
||||||
authorID: f.UserID,
|
authorID: f.UserID,
|
||||||
authorURL: f.Username.GetIconURL(),
|
authorURL: f.Username.GetIconURL(),
|
||||||
nonce: f.generateNonce(),
|
nonce: f.generateNonce(),
|
||||||
|
replyID: f.replyingID,
|
||||||
files: attachments,
|
files: attachments,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -94,23 +95,38 @@ func (f *Field) SendMessage(data PresendMessage) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Files is a list of attachments.
|
||||||
|
type Files []attachment.File
|
||||||
|
|
||||||
|
// Attachments returns the list of files as a list of cchat attachments.
|
||||||
|
func (files Files) Attachments() []cchat.MessageAttachment {
|
||||||
|
var attachments = make([]cchat.MessageAttachment, len(files))
|
||||||
|
for i, file := range files {
|
||||||
|
attachments[i] = file.AsAttachment()
|
||||||
|
}
|
||||||
|
return attachments
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageData contains what is to be sent in a message. It behaves
|
||||||
|
// 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 text.Rich
|
||||||
authorID string
|
authorID cchat.ID
|
||||||
authorURL string // avatar
|
authorURL string // avatar
|
||||||
nonce string
|
nonce string
|
||||||
files []attachment.File
|
replyID cchat.ID
|
||||||
|
files Files
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cchat.SendableMessage = (*SendMessageData)(nil)
|
var _ cchat.SendableMessage = (*SendMessageData)(nil)
|
||||||
|
|
||||||
|
// PresendMessage is an interface for any message about to be sent.
|
||||||
type PresendMessage interface {
|
type PresendMessage interface {
|
||||||
cchat.MessageHeader // returns nonce and time
|
cchat.MessageHeader // returns nonce and time
|
||||||
cchat.SendableMessage
|
cchat.SendableMessage
|
||||||
cchat.Noncer
|
cchat.Noncer
|
||||||
cchat.Attachments
|
|
||||||
|
|
||||||
// These methods are reserved for internal use.
|
// These methods are reserved for internal use.
|
||||||
|
|
||||||
|
|
@ -123,50 +139,15 @@ type PresendMessage interface {
|
||||||
var _ PresendMessage = (*SendMessageData)(nil)
|
var _ PresendMessage = (*SendMessageData)(nil)
|
||||||
|
|
||||||
// ID returns a pseudo ID for internal use.
|
// ID returns a pseudo ID for internal use.
|
||||||
func (s SendMessageData) ID() string {
|
func (s SendMessageData) ID() string { return s.nonce }
|
||||||
return s.nonce
|
func (s SendMessageData) Time() time.Time { return s.time }
|
||||||
}
|
func (s SendMessageData) Content() string { return s.content }
|
||||||
|
func (s SendMessageData) Author() text.Rich { return s.author }
|
||||||
func (s SendMessageData) Time() time.Time {
|
func (s SendMessageData) AuthorID() string { return s.authorID }
|
||||||
return s.time
|
func (s SendMessageData) AuthorAvatarURL() string { return s.authorURL }
|
||||||
}
|
func (s SendMessageData) AsNoncer() cchat.Noncer { return s }
|
||||||
|
func (s SendMessageData) Nonce() string { return s.nonce }
|
||||||
func (s SendMessageData) Content() string {
|
func (s SendMessageData) Files() []attachment.File { return s.files }
|
||||||
return s.content
|
func (s SendMessageData) AsAttacher() cchat.Attacher { return s.files }
|
||||||
}
|
func (s SendMessageData) AsReplier() cchat.Replier { return s }
|
||||||
|
func (s SendMessageData) ReplyingTo() cchat.ID { return s.replyID }
|
||||||
func (s SendMessageData) Author() text.Rich {
|
|
||||||
return s.author
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SendMessageData) AuthorID() string {
|
|
||||||
return s.authorID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SendMessageData) AuthorAvatarURL() string {
|
|
||||||
return s.authorURL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SendMessageData) AsNoncer() cchat.Noncer {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SendMessageData) Nonce() string {
|
|
||||||
return s.nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SendMessageData) Files() []attachment.File {
|
|
||||||
return s.files
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SendMessageData) AsAttachments() cchat.Attachments {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SendMessageData) Attachments() []cchat.MessageAttachment {
|
|
||||||
var attachments = make([]cchat.MessageAttachment, len(s.files))
|
|
||||||
for i, file := range s.files {
|
|
||||||
attachments[i] = file.AsAttachment()
|
|
||||||
}
|
|
||||||
return attachments
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,11 @@ func (u *Container) GetLabel() text.Rich {
|
||||||
return u.label.GetLabel()
|
return u.label.GetLabel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLabelMarkup is not thread-safe.
|
||||||
|
func (u *Container) GetLabelMarkup() string {
|
||||||
|
return u.label.Label.GetLabel()
|
||||||
|
}
|
||||||
|
|
||||||
// SetLabel is thread-safe.
|
// SetLabel is thread-safe.
|
||||||
func (u *Container) SetLabel(content text.Rich) {
|
func (u *Container) SetLabel(content text.Rich) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ type Container interface {
|
||||||
Time() time.Time
|
Time() time.Time
|
||||||
AuthorID() string
|
AuthorID() string
|
||||||
AuthorName() string
|
AuthorName() string
|
||||||
|
AuthorMarkup() string
|
||||||
AvatarURL() string // avatar
|
AvatarURL() string // avatar
|
||||||
Nonce() string
|
Nonce() string
|
||||||
|
|
||||||
|
|
@ -48,6 +49,10 @@ func RefreshContainer(c Container, gc *GenericContainer) {
|
||||||
// GenericContainer provides a single generic message container for subpackages
|
// GenericContainer provides a single generic message container for subpackages
|
||||||
// to use.
|
// to use.
|
||||||
type GenericContainer struct {
|
type GenericContainer struct {
|
||||||
|
*gtk.Box
|
||||||
|
row *gtk.ListBoxRow // contains Box
|
||||||
|
class string
|
||||||
|
|
||||||
id string
|
id string
|
||||||
time time.Time
|
time time.Time
|
||||||
authorID string
|
authorID string
|
||||||
|
|
@ -67,7 +72,7 @@ type GenericContainer struct {
|
||||||
|
|
||||||
var _ Container = (*GenericContainer)(nil)
|
var _ Container = (*GenericContainer)(nil)
|
||||||
|
|
||||||
var timestampCSS = primitives.PrepareCSS(`
|
var timestampCSS = primitives.PrepareClassCSS("message-time", `
|
||||||
.message-time {
|
.message-time {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
|
@ -76,6 +81,12 @@ var timestampCSS = primitives.PrepareCSS(`
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
@ -91,14 +102,12 @@ func NewContainer(msg cchat.MessageCreate) *GenericContainer {
|
||||||
func NewEmptyContainer() *GenericContainer {
|
func NewEmptyContainer() *GenericContainer {
|
||||||
ts, _ := gtk.LabelNew("")
|
ts, _ := gtk.LabelNew("")
|
||||||
ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
||||||
ts.SetXAlign(1) // right align
|
ts.SetXAlign(0.5) // centre align
|
||||||
ts.SetVAlign(gtk.ALIGN_END)
|
ts.SetVAlign(gtk.ALIGN_END)
|
||||||
ts.Show()
|
ts.Show()
|
||||||
|
|
||||||
user := labeluri.NewLabel(text.Rich{})
|
user := labeluri.NewLabel(text.Rich{})
|
||||||
user.SetLineWrap(true)
|
user.SetXAlign(0) // left align
|
||||||
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
|
||||||
user.SetXAlign(1) // right align
|
|
||||||
user.SetVAlign(gtk.ALIGN_START)
|
user.SetVAlign(gtk.ALIGN_START)
|
||||||
user.SetTrackVisitedLinks(false)
|
user.SetTrackVisitedLinks(false)
|
||||||
user.Show()
|
user.Show()
|
||||||
|
|
@ -117,26 +126,25 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
ctbox.PackStart(ctbody, false, false, 0)
|
ctbox.PackStart(ctbody, false, false, 0)
|
||||||
ctbox.Show()
|
ctbox.Show()
|
||||||
|
|
||||||
// Causes bugs with selections.
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
box.Show()
|
||||||
|
|
||||||
// ctbody.Connect("grab-notify", func(l *gtk.Label, grabbed bool) {
|
row, _ := gtk.ListBoxRowNew()
|
||||||
// if grabbed {
|
row.Add(box)
|
||||||
// // Hack to stop the label from selecting everything after being
|
row.Show()
|
||||||
// // refocused.
|
|
||||||
// ctbody.SetSelectable(false)
|
|
||||||
// gts.ExecAsync(func() { ctbody.SetSelectable(true) })
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// Add CSS classes.
|
// Add CSS classes.
|
||||||
primitives.AddClass(ts, "message-time")
|
primitives.AddClass(ts, "message-time")
|
||||||
|
primitives.AddClass(row, "message-row")
|
||||||
primitives.AddClass(user, "message-author")
|
primitives.AddClass(user, "message-author")
|
||||||
primitives.AddClass(ctbody, "message-content")
|
primitives.AddClass(ctbody, "message-content")
|
||||||
|
timestampCSS(ts)
|
||||||
// Attach the timestamp CSS.
|
authorCSS(ts)
|
||||||
primitives.AttachCSS(ts, timestampCSS)
|
|
||||||
|
|
||||||
gc := &GenericContainer{
|
gc := &GenericContainer{
|
||||||
|
Box: box,
|
||||||
|
row: row,
|
||||||
|
|
||||||
Timestamp: ts,
|
Timestamp: ts,
|
||||||
Username: user,
|
Username: user,
|
||||||
Content: ctbox,
|
Content: ctbox,
|
||||||
|
|
@ -157,6 +165,25 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
return gc
|
return gc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Row returns the internal list box row. It is used to satisfy MessageRow.
|
||||||
|
func (m *GenericContainer) Row() *gtk.ListBoxRow { return m.row }
|
||||||
|
|
||||||
|
// SetClass sets the internal row's class.
|
||||||
|
func (m *GenericContainer) SetClass(class string) {
|
||||||
|
if m.class != "" {
|
||||||
|
primitives.RemoveClass(m.row, m.class)
|
||||||
|
}
|
||||||
|
|
||||||
|
primitives.AddClass(m.row, class)
|
||||||
|
m.class = class
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReferenceHighlighter sets the reference highlighter into the message.
|
||||||
|
func (m *GenericContainer) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
|
||||||
|
m.Username.SetReferenceHighlighter(r)
|
||||||
|
m.ContentBody.SetReferenceHighlighter(r)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) ID() string {
|
func (m *GenericContainer) ID() string {
|
||||||
return m.id
|
return m.id
|
||||||
}
|
}
|
||||||
|
|
@ -173,6 +200,10 @@ func (m *GenericContainer) AuthorName() string {
|
||||||
return m.authorName
|
return m.authorName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *GenericContainer) AuthorMarkup() string {
|
||||||
|
return m.Username.Label.Label.GetLabel()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) AvatarURL() string {
|
func (m *GenericContainer) AvatarURL() string {
|
||||||
return m.avatarURL
|
return m.avatarURL
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +226,7 @@ func (m *GenericContainer) UpdateAuthor(author cchat.Author) {
|
||||||
|
|
||||||
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
|
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
|
||||||
cfg := markup.RenderConfig{}
|
cfg := markup.RenderConfig{}
|
||||||
|
cfg.NoReferencing = true
|
||||||
cfg.SetForegroundAnchor(m.ContentBody)
|
cfg.SetForegroundAnchor(m.ContentBody)
|
||||||
|
|
||||||
m.authorName = name.String()
|
m.authorName = name.String()
|
||||||
|
|
|
||||||
|
|
@ -31,26 +31,18 @@ var dotsCSS = primitives.PrepareCSS(`
|
||||||
const breathingChar = "●"
|
const breathingChar = "●"
|
||||||
|
|
||||||
func NewDots() *gtk.Box {
|
func NewDots() *gtk.Box {
|
||||||
c1, _ := gtk.LabelNew(breathingChar)
|
|
||||||
c1.Show()
|
|
||||||
c2, _ := gtk.LabelNew(breathingChar)
|
|
||||||
c2.Show()
|
|
||||||
c3, _ := gtk.LabelNew(breathingChar)
|
|
||||||
c3.Show()
|
|
||||||
|
|
||||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
b.Add(c1)
|
|
||||||
b.Add(c2)
|
|
||||||
b.Add(c3)
|
|
||||||
|
|
||||||
primitives.AddClass(b, "breathing-dots")
|
primitives.AddClass(b, "breathing-dots")
|
||||||
|
|
||||||
primitives.AttachCSS(c1, dotsCSS)
|
for i := 0; i < 3; i++ {
|
||||||
primitives.AttachCSS(c1, smallfonts)
|
c, _ := gtk.LabelNew(breathingChar)
|
||||||
primitives.AttachCSS(c2, dotsCSS)
|
c.Show()
|
||||||
primitives.AttachCSS(c2, smallfonts)
|
|
||||||
primitives.AttachCSS(c3, dotsCSS)
|
primitives.AttachCSS(c, dotsCSS)
|
||||||
primitives.AttachCSS(c3, smallfonts)
|
primitives.AttachCSS(c, smallfonts)
|
||||||
|
|
||||||
|
b.Add(c)
|
||||||
|
}
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ type State struct {
|
||||||
|
|
||||||
// consts
|
// consts
|
||||||
changed func(s *State, empty bool)
|
changed func(s *State, empty bool)
|
||||||
stopper func() // stops the event loop, not used atm
|
stopper func()
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cchat.TypingContainer = (*State)(nil)
|
var _ cchat.TypingContainer = (*State)(nil)
|
||||||
|
|
@ -45,7 +45,7 @@ func (s *State) Subscribe(indicator cchat.TypingIndicator) {
|
||||||
gts.Async(func() (func(), error) {
|
gts.Async(func() (func(), error) {
|
||||||
c, err := indicator.TypingSubscribe(s)
|
c, err := indicator.TypingSubscribe(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Failed to subscribe to typing indicator")
|
return nil, errors.Wrap(err, "failed to subscribe to typing indicator")
|
||||||
}
|
}
|
||||||
|
|
||||||
return func() {
|
return func() {
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,25 @@ import (
|
||||||
"github.com/gotk3/gotk3/pango"
|
"github.com/gotk3/gotk3/pango"
|
||||||
)
|
)
|
||||||
|
|
||||||
var typingIndicatorCSS = primitives.PrepareCSS(`
|
var typingIndicatorCSS = primitives.PrepareClassCSS("typing-indicator", `
|
||||||
.typing-indicator {
|
.typing-indicator {
|
||||||
margin: 0 6px;
|
margin: 0 6px;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
|
|
||||||
color: alpha(@theme_fg_color, 0.8);
|
color: alpha(@theme_fg_color, 0.8);
|
||||||
background-color: @theme_base_color;
|
background-color: @theme_base_color;
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
var typingLabelCSS = primitives.PrepareClassCSS("typing-label", `
|
||||||
|
.typing-label {
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
var smallfonts = primitives.PrepareCSS(`
|
var smallfonts = primitives.PrepareCSS(`
|
||||||
* { font-size: 0.9em; }
|
* { font-size: 0.9em; }
|
||||||
`)
|
`)
|
||||||
|
|
@ -27,6 +36,14 @@ var smallfonts = primitives.PrepareCSS(`
|
||||||
type Container struct {
|
type Container struct {
|
||||||
*gtk.Revealer
|
*gtk.Revealer
|
||||||
state *State
|
state *State
|
||||||
|
|
||||||
|
dots *gtk.Box
|
||||||
|
label *gtk.Label
|
||||||
|
|
||||||
|
// borrow, if true, will not update the label until it is set to false.
|
||||||
|
borrow bool
|
||||||
|
// markup stores the label if the label view is not borrowed.
|
||||||
|
markup string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Container {
|
func New() *Container {
|
||||||
|
|
@ -37,10 +54,11 @@ func New() *Container {
|
||||||
l.SetXAlign(0)
|
l.SetXAlign(0)
|
||||||
l.SetEllipsize(pango.ELLIPSIZE_END)
|
l.SetEllipsize(pango.ELLIPSIZE_END)
|
||||||
l.Show()
|
l.Show()
|
||||||
|
typingLabelCSS(l)
|
||||||
primitives.AttachCSS(l, smallfonts)
|
primitives.AttachCSS(l, smallfonts)
|
||||||
|
|
||||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
b.PackStart(d, false, false, 4)
|
b.PackStart(d, false, false, 0)
|
||||||
b.PackStart(l, true, true, 0)
|
b.PackStart(l, true, true, 0)
|
||||||
b.Show()
|
b.Show()
|
||||||
|
|
||||||
|
|
@ -50,21 +68,31 @@ func New() *Container {
|
||||||
r.SetRevealChild(false)
|
r.SetRevealChild(false)
|
||||||
r.Add(b)
|
r.Add(b)
|
||||||
|
|
||||||
primitives.AddClass(b, "typing-indicator")
|
typingIndicatorCSS(b)
|
||||||
primitives.AttachCSS(b, typingIndicatorCSS)
|
|
||||||
|
|
||||||
state := NewState(func(s *State, empty bool) {
|
container := &Container{
|
||||||
r.SetRevealChild(!empty)
|
Revealer: r,
|
||||||
l.SetMarkup(render(s.typers))
|
dots: d,
|
||||||
|
label: l,
|
||||||
|
}
|
||||||
|
|
||||||
|
container.state = NewState(func(s *State, empty bool) {
|
||||||
|
if !empty {
|
||||||
|
container.markup = render(s.typers)
|
||||||
|
} else {
|
||||||
|
container.markup = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if !container.borrow {
|
||||||
|
r.SetRevealChild(!empty)
|
||||||
|
l.SetMarkup(container.markup)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// On label destroy, stop the state loop as well.
|
// On label destroy, stop the state loop as well.
|
||||||
l.Connect("destroy", func(interface{}) { state.stopper() })
|
l.Connect("destroy", func(interface{}) { container.state.stopper() })
|
||||||
|
|
||||||
return &Container{
|
return container
|
||||||
Revealer: r,
|
|
||||||
state: state,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) Reset() {
|
func (c *Container) Reset() {
|
||||||
|
|
@ -72,6 +100,28 @@ func (c *Container) Reset() {
|
||||||
c.SetRevealChild(false)
|
c.SetRevealChild(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BorrowLabel borrows the container label. The typing indicator will display
|
||||||
|
// the given markup string instead of the markup it is intended to display until
|
||||||
|
// Unborrow is called.
|
||||||
|
func (c *Container) BorrowLabel(markup string) {
|
||||||
|
c.borrow = true
|
||||||
|
c.label.SetMarkup(markup)
|
||||||
|
c.dots.Hide() // bad, TODO use revealer
|
||||||
|
c.SetRevealChild(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unborrow stops borrowing the typing indicator, returning it to the state it
|
||||||
|
// is supposed to show. Calling Unborrow multiple times will only take effect
|
||||||
|
// for the first time.
|
||||||
|
func (c *Container) Unborrow() {
|
||||||
|
if c.borrow {
|
||||||
|
c.label.SetMarkup(c.markup)
|
||||||
|
c.SetRevealChild(c.markup != "")
|
||||||
|
c.dots.Show() // bad, TODO use revealer
|
||||||
|
c.borrow = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) RemoveAuthor(author cchat.Author) {
|
func (c *Container) RemoveAuthor(author cchat.Author) {
|
||||||
c.state.removeTyper(author.ID())
|
c.state.removeTyper(author.ID())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ func NewView(c Controller) *View {
|
||||||
sep.SetHExpand(true)
|
sep.SetHExpand(true)
|
||||||
sep.Show()
|
sep.Show()
|
||||||
|
|
||||||
view.InputView = input.NewView(view)
|
view.InputView = input.NewView(view, view.Typing)
|
||||||
view.InputView.SetHExpand(true)
|
view.InputView.SetHExpand(true)
|
||||||
view.InputView.Show()
|
view.InputView.Show()
|
||||||
|
|
||||||
|
|
@ -363,10 +363,7 @@ func (v *View) FetchBacklog() {
|
||||||
|
|
||||||
var done = func() {
|
var done = func() {
|
||||||
v.ctrl.OnMessageDone()
|
v.ctrl.OnMessageDone()
|
||||||
|
v.Container.Highlight(firstMsg)
|
||||||
// Restore scrolling.
|
|
||||||
y := v.Container.TranslateCoordinates(v.MsgBox, firstMsg)
|
|
||||||
v.Scroller.GetVAdjustment().SetValue(float64(y))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gts.Async(func() (func(), error) {
|
gts.Async(func() (func(), error) {
|
||||||
|
|
@ -403,13 +400,22 @@ func (v *View) AuthorEvent(author cchat.Author) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *View) MessageAuthorMarkup(msgID cchat.ID) (string, bool) {
|
||||||
|
msg := v.Container.Message(msgID, "")
|
||||||
|
if msg == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg.AuthorMarkup(), true
|
||||||
|
}
|
||||||
|
|
||||||
// LatestMessageFrom returns the last message ID with that author.
|
// LatestMessageFrom returns the last message ID with that author.
|
||||||
func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
||||||
return v.Container.LatestMessageFrom(userID)
|
return v.Container.LatestMessageFrom(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// retryMessage sends the message.
|
// retryMessage sends the message.
|
||||||
func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendGridMessage) {
|
func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendMessageRow) {
|
||||||
var sender = v.InputView.Sender
|
var sender = v.InputView.Sender
|
||||||
if sender == nil {
|
if sender == nil {
|
||||||
return
|
return
|
||||||
|
|
@ -426,9 +432,13 @@ func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendG
|
||||||
|
|
||||||
// BindMenu attaches the menu constructor into the message with the needed
|
// BindMenu attaches the menu constructor into the message with the needed
|
||||||
// states and callbacks.
|
// states and callbacks.
|
||||||
func (v *View) BindMenu(msg container.GridMessage) {
|
func (v *View) BindMenu(msg container.MessageRow) {
|
||||||
// Add 1 for the edit menu item.
|
// Add 1 for the edit menu item.
|
||||||
var mitems []menu.Item
|
var mitems = []menu.Item{
|
||||||
|
menu.SimpleItem(
|
||||||
|
"Reply", func() { v.InputView.StartReplyingTo(msg.ID()) },
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
// Do we have editing capabilities? If yes, append a button to allow it.
|
// Do we have editing capabilities? If yes, append a button to allow it.
|
||||||
if v.InputView.Editable(msg.ID()) {
|
if v.InputView.Editable(msg.ID()) {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@ package actions
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/glib"
|
"github.com/gotk3/gotk3/glib"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActionGroupInserter interface {
|
type ActionGroupInserter interface {
|
||||||
|
primitives.Connector
|
||||||
InsertActionGroup(prefix string, action glib.IActionGroup)
|
InsertActionGroup(prefix string, action glib.IActionGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +43,15 @@ func (m *Menu) InsertActionGroup(w ActionGroupInserter) {
|
||||||
w.InsertActionGroup(m.prefix, m)
|
w.InsertActionGroup(m.prefix, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Menu) BindRightClick(w ActionGroupInserter) {
|
||||||
|
m.InsertActionGroup(w)
|
||||||
|
w.Connect("button-press-event", func(w gtk.IWidget, ev *gdk.Event) {
|
||||||
|
if gts.EventIsRightClick(ev) {
|
||||||
|
m.Popup(w)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Popup pops up the menu popover. It does not pop up anything if there are no
|
// Popup pops up the menu popover. It does not pop up anything if there are no
|
||||||
// menu items.
|
// menu items.
|
||||||
func (m *Menu) Popup(relative gtk.IWidget) {
|
func (m *Menu) Popup(relative gtk.IWidget) {
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ type LazyMenu struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLazyMenu(bindTo primitives.Connector) *LazyMenu {
|
func NewLazyMenu(bindTo primitives.Connector) *LazyMenu {
|
||||||
l := &LazyMenu{}
|
l := LazyMenu{}
|
||||||
bindTo.Connect("button-press-event", l.popup)
|
bindTo.Connect("button-press-event", l.popup)
|
||||||
return l
|
return &l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *LazyMenu) popup(w gtk.IWidget, ev *gdk.Event) {
|
func (m *LazyMenu) popup(w gtk.IWidget, ev *gdk.Event) {
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,14 @@ func HandleDestroyCtx(ctx context.Context, connector Connector) context.Context
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func OnRightClick(connector Connector, fn func()) {
|
||||||
|
connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) {
|
||||||
|
if gts.EventIsRightClick(ev) {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func BindMenu(connector Connector, menu *gtk.Menu) {
|
func BindMenu(connector Connector, menu *gtk.Menu) {
|
||||||
connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) {
|
connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) {
|
||||||
if gts.EventIsRightClick(ev) {
|
if gts.EventIsRightClick(ev) {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ type Labeler interface {
|
||||||
// Label implements a label that's already bounded to the markup URI handlers.
|
// Label implements a label that's already bounded to the markup URI handlers.
|
||||||
type Label struct {
|
type Label struct {
|
||||||
*rich.Label
|
*rich.Label
|
||||||
|
*BoundBox
|
||||||
output markup.RenderOutput
|
output markup.RenderOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +63,7 @@ func NewLabel(txt text.Rich) *Label {
|
||||||
l.Label.SetLabelUnsafe(txt) // test
|
l.Label.SetLabelUnsafe(txt) // test
|
||||||
|
|
||||||
// Bind and return.
|
// Bind and return.
|
||||||
BindRichLabel(l)
|
l.BoundBox = BindRichLabel(l)
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,21 +88,50 @@ func (l *Label) SetOutput(o markup.RenderOutput) {
|
||||||
l.SetMarkup(o.Markup)
|
l.SetMarkup(o.Markup)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BindRichLabel(label Labeler) {
|
type ReferenceHighlighter interface {
|
||||||
bind(label, func(uri string, ptr gdk.Rectangle) bool {
|
HighlightReference(ref markup.ReferenceSegment)
|
||||||
var output = label.Output()
|
}
|
||||||
|
|
||||||
if segment := output.IsMention(uri); segment != nil {
|
// BoundBox is a box wrapping elements that can be interacted with from the
|
||||||
if p := NewPopoverMentioner(label, output.Input, segment); p != nil {
|
// parsed labels.
|
||||||
p.SetPointingTo(ptr)
|
type BoundBox struct {
|
||||||
p.Popup()
|
label Labeler
|
||||||
}
|
refer ReferenceHighlighter
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
func BindRichLabel(label Labeler) *BoundBox {
|
||||||
|
bound := BoundBox{label: label}
|
||||||
|
bind(label, bound.activate)
|
||||||
|
return &bound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bound *BoundBox) activate(uri string, ptr gdk.Rectangle) bool {
|
||||||
|
var output = bound.label.Output()
|
||||||
|
|
||||||
|
switch segment := output.URISegment(uri).(type) {
|
||||||
|
case markup.MentionSegment:
|
||||||
|
popover := NewPopoverMentioner(bound.label, output.Input, segment)
|
||||||
|
if popover != nil {
|
||||||
|
popover.SetPointingTo(ptr)
|
||||||
|
popover.Popup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
case markup.ReferenceSegment:
|
||||||
|
if bound.refer != nil {
|
||||||
|
bound.refer.HighlightReference(segment)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
default:
|
||||||
return false
|
return false
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bound *BoundBox) SetReferenceHighlighter(refer ReferenceHighlighter) {
|
||||||
|
bound.refer = refer
|
||||||
}
|
}
|
||||||
|
|
||||||
func PopoverMentioner(rel gtk.IWidget, input string, mention text.Segment) {
|
func PopoverMentioner(rel gtk.IWidget, input string, mention text.Segment) {
|
||||||
|
|
|
||||||
|
|
@ -45,32 +45,38 @@ type ReferenceSegment struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// f_Mention is used to print and parse mention URIs.
|
MentionType = "mention"
|
||||||
f_Mention = "cchat://mention/%d" // %d == Mentions[i]
|
ReferenceType = "reference"
|
||||||
f_Reference = "cchat://reference/%d" // %d == References[i]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsMention returns the mention if the URI is correct, or nil if none.
|
func fmtSegmentURI(stype string, ix int) string {
|
||||||
func (r RenderOutput) IsMention(uri string) text.Segment {
|
u := url.URL{
|
||||||
var i int
|
Scheme: "cchat",
|
||||||
|
Host: stype,
|
||||||
_, err := fmt.Sscanf(uri, f_Mention, &i)
|
Path: strconv.Itoa(ix),
|
||||||
if err != nil || i >= len(r.Mentions) {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return u.String()
|
||||||
return r.Mentions[i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r RenderOutput) IsReference(uri string) text.Segment {
|
func (r RenderOutput) URISegment(uri string) text.Segment {
|
||||||
var i int
|
u, err := url.Parse(uri)
|
||||||
|
if err != nil || u.Scheme != "cchat" {
|
||||||
_, err := fmt.Sscanf(uri, f_Reference, &i)
|
|
||||||
if err != nil || i >= len(r.References) {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.References[i]
|
i, err := strconv.Atoi(strings.TrimPrefix(u.Path, "/"))
|
||||||
|
if err != nil {
|
||||||
|
panic("Invalid path " + u.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch u.Host {
|
||||||
|
case MentionType:
|
||||||
|
return r.Mentions[i]
|
||||||
|
case ReferenceType:
|
||||||
|
return r.References[i]
|
||||||
|
default:
|
||||||
|
panic("Unknown internal URI ID: " + u.Host)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Render(content text.Rich) string {
|
func Render(content text.Rich) string {
|
||||||
|
|
@ -86,6 +92,10 @@ type RenderConfig struct {
|
||||||
// NoMentionLinks, if true, will not render any mentions.
|
// NoMentionLinks, if true, will not render any mentions.
|
||||||
NoMentionLinks bool
|
NoMentionLinks bool
|
||||||
|
|
||||||
|
// NoReferencing, if true, will not parse reference links and prefer
|
||||||
|
// mentions.
|
||||||
|
NoReferencing bool
|
||||||
|
|
||||||
// AnchorColor forces all anchors to be of a certain color. This is used if
|
// AnchorColor forces all anchors to be of a certain color. This is used if
|
||||||
// the boolean is true. Else, all mention links will not work and regular
|
// the boolean is true. Else, all mention links will not work and regular
|
||||||
// links will be of the default color.
|
// links will be of the default color.
|
||||||
|
|
@ -176,7 +186,7 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
|
||||||
if mentioner := segment.AsMentioner(); mentioner != nil && !cfg.NoMentionLinks {
|
if mentioner := segment.AsMentioner(); mentioner != nil && !cfg.NoMentionLinks {
|
||||||
// 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, fmt.Sprintf(f_Mention, len(mentions)))
|
appended.AnchorNU(start, end, fmtSegmentURI(MentionType, len(mentions)))
|
||||||
hasAnchor = true
|
hasAnchor = true
|
||||||
|
|
||||||
// Add the mention segment into the list regardless of hyperlinks.
|
// Add the mention segment into the list regardless of hyperlinks.
|
||||||
|
|
@ -213,16 +223,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 reference := segment.AsMessageReferencer(); !hasAnchor && reference != nil {
|
if !cfg.NoReferencing && !hasAnchor {
|
||||||
// Render the mention into "cchat://reference:0" or such. Other
|
if reference := segment.AsMessageReferencer(); reference != nil {
|
||||||
// components will take care of showing the information.
|
// Render the mention into "cchat://reference:0" or such. Other
|
||||||
appended.AnchorNU(start, end, fmt.Sprintf(f_Reference, len(references)))
|
// components will take care of showing the information.
|
||||||
|
appended.AnchorNU(start, end, fmtSegmentURI(ReferenceType, len(references)))
|
||||||
|
|
||||||
// Add the mention segment into the list regardless of hyperlinks.
|
// Add the mention segment into the list regardless of hyperlinks.
|
||||||
references = append(references, ReferenceSegment{
|
references = append(references, ReferenceSegment{
|
||||||
Segment: segment,
|
Segment: segment,
|
||||||
MessageReferencer: reference,
|
MessageReferencer: reference,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if attributor := segment.AsAttributor(); attributor != nil {
|
if attributor := segment.AsAttributor(); attributor != nil {
|
||||||
|
|
|
||||||
|
|
@ -3,38 +3,95 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Configurator interface {
|
type Configurator struct {
|
||||||
cchat.Service
|
cchat.Service
|
||||||
cchat.Configurator
|
cchat.Configurator
|
||||||
}
|
}
|
||||||
|
|
||||||
func MenuItem(conf Configurator) menu.Item {
|
func MenuItem(conf Configurator) menu.Item {
|
||||||
return menu.SimpleItem("Configure", func() {
|
return menu.SimpleItem("Configure", func() { Spawn(conf) })
|
||||||
SpawnConfigurator(conf)
|
}
|
||||||
|
|
||||||
|
// Restore restores the config in the background.
|
||||||
|
func Restore(conf Configurator) {
|
||||||
|
gts.Async(func() (func(), error) {
|
||||||
|
c, err := conf.Configuration()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to get %s config", conf.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
file := serviceFile(conf)
|
||||||
|
|
||||||
|
if err := config.UnmarshalFromFile(file, c); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to unmarshal %s config", conf.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.SetConfiguration(c); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to set %s config", conf.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func SpawnConfigurator(conf Configurator) error {
|
func Spawn(conf Configurator) error {
|
||||||
c, err := conf.Configuration()
|
gts.Async(func() (func(), error) {
|
||||||
if err != nil {
|
c, err := conf.Configuration()
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return nil, errors.Wrapf(err, "failed to get %s config", conf.Name())
|
||||||
|
}
|
||||||
|
|
||||||
Spawn(conf.Name().Content, c, func() error {
|
file := serviceFile(conf)
|
||||||
return conf.SetConfiguration(c)
|
|
||||||
|
err = config.UnmarshalFromFile(file, c)
|
||||||
|
err = errors.Wrapf(err, "failed to unmarshal %s config", conf.Name())
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
spawn(conf.Name().String(), c, func(finalized bool) error {
|
||||||
|
if err := conf.SetConfiguration(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalized {
|
||||||
|
gts.Async(func() (func(), error) {
|
||||||
|
return nil, config.MarshalToFile(file, c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}, err
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Spawn(name string, conf map[string]string, apply func() error) {
|
func serviceFile(conf Configurator) string {
|
||||||
container := newContainer(conf, apply)
|
return fmt.Sprintf("service-%s.json", dumbHash(conf.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumbHash(name text.Rich) string {
|
||||||
|
hash := fnv.New32a()
|
||||||
|
io.WriteString(hash, name.String())
|
||||||
|
return strconv.FormatUint(uint64(hash.Sum32()), 36)
|
||||||
|
}
|
||||||
|
|
||||||
|
func spawn(name string, conf map[string]string, apply func(final bool) error) {
|
||||||
|
container := newContainer(conf, func() error { return apply(false) })
|
||||||
container.Grid.SetVAlign(gtk.ALIGN_START)
|
container.Grid.SetVAlign(gtk.ALIGN_START)
|
||||||
|
|
||||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||||
|
|
@ -62,5 +119,8 @@ func Spawn(name string, conf map[string]string, apply func() error) {
|
||||||
d.Add(b)
|
d.Add(b)
|
||||||
d.SetTitle(title)
|
d.SetTitle(title)
|
||||||
d.SetTitlebar(h)
|
d.SetTitlebar(h)
|
||||||
|
|
||||||
|
d.Connect("destroy", func(*gtk.Dialog) { apply(true) })
|
||||||
|
|
||||||
d.Show()
|
d.Show()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/keyring"
|
"github.com/diamondburned/cchat-gtk/internal/keyring"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/actions"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag"
|
||||||
"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"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/config"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||||
|
|
@ -42,11 +44,13 @@ type Service struct {
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
Button *gtk.ToggleButton
|
Button *gtk.ToggleButton
|
||||||
Icon *rich.Icon
|
Icon *rich.Icon
|
||||||
|
Menu *actions.Menu
|
||||||
|
|
||||||
BodyRev *gtk.Revealer // revealed
|
BodyRev *gtk.Revealer // revealed
|
||||||
BodyList *session.List // not really supposed to be here
|
BodyList *session.List // not really supposed to be here
|
||||||
|
|
||||||
service cchat.Service // state
|
service cchat.Service // state
|
||||||
|
Configurator cchat.Configurator
|
||||||
}
|
}
|
||||||
|
|
||||||
var serviceCSS = primitives.PrepareClassCSS("service", `
|
var serviceCSS = primitives.PrepareClassCSS("service", `
|
||||||
|
|
@ -127,6 +131,20 @@ func NewService(svc cchat.Service, svclctrl ListController) *Service {
|
||||||
})
|
})
|
||||||
serviceButtonCSS(service.Button)
|
serviceButtonCSS(service.Button)
|
||||||
|
|
||||||
|
// Bind session.* actions into row.
|
||||||
|
service.Menu = actions.NewMenu("service")
|
||||||
|
// Bind right clicks and show a popover menu on such event.
|
||||||
|
service.Menu.BindRightClick(service.Button)
|
||||||
|
|
||||||
|
if configurator := svc.AsConfigurator(); configurator != nil {
|
||||||
|
cfg := config.Configurator{
|
||||||
|
Service: svc,
|
||||||
|
Configurator: configurator,
|
||||||
|
}
|
||||||
|
config.Restore(cfg)
|
||||||
|
service.Menu.AddAction("Configure", func() { config.Spawn(cfg) })
|
||||||
|
}
|
||||||
|
|
||||||
// Intermediary box to contain both the icon and the revealer.
|
// Intermediary box to contain both the icon and the revealer.
|
||||||
service.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
service.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
service.Box.PackStart(service.Button, false, false, 0)
|
service.Box.PackStart(service.Button, false, false, 0)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/commander"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/commander"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
@ -176,10 +175,8 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Controller) *Row
|
||||||
row.ActionsMenu.InsertActionGroup(row)
|
row.ActionsMenu.InsertActionGroup(row)
|
||||||
|
|
||||||
// Bind right clicks and show a popover menu on such event.
|
// Bind right clicks and show a popover menu on such event.
|
||||||
row.iconBox.Connect("button-press-event", func(_ interface{}, ev *gdk.Event) {
|
primitives.OnRightClick(row.iconBox, func() {
|
||||||
if gts.EventIsRightClick(ev) {
|
row.ActionsMenu.Popup(row)
|
||||||
row.ActionsMenu.Popup(row)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Bind drag-and-drop events.
|
// Bind drag-and-drop events.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue