Compare commits
4 Commits
bfad966cd2
...
bcd2de2e49
Author | SHA1 | Date |
---|---|---|
diamondburned | bcd2de2e49 | |
diamondburned | 5151d47427 | |
diamondburned | 1247f9d48f | |
diamondburned | 150329e5dd |
4
go.mod
4
go.mod
|
@ -2,7 +2,7 @@ module github.com/diamondburned/cchat-gtk
|
||||||
|
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20201230071527-a77c32eb3876
|
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20210103063209-2bfa1d9dd9a6
|
||||||
|
|
||||||
// replace github.com/diamondburned/cchat-discord => ../cchat-discord
|
// replace github.com/diamondburned/cchat-discord => ../cchat-discord
|
||||||
// replace github.com/diamondburned/gotk3-tcmalloc => ../../gotk3-tcmalloc
|
// replace github.com/diamondburned/gotk3-tcmalloc => ../../gotk3-tcmalloc
|
||||||
|
@ -13,7 +13,7 @@ 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.17
|
github.com/diamondburned/cchat v0.3.17
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20210102085253-a691813b9041
|
github.com/diamondburned/cchat-discord v0.0.0-20210103044430-14970d0e05eb
|
||||||
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
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -80,6 +80,8 @@ github.com/diamondburned/cchat-discord v0.0.0-20210102040711-73b0d3f39c41 h1:cUV
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20210102040711-73b0d3f39c41/go.mod h1:KL3i+ER58BrJ8JBkpy6WQ0mDZdlkgz7KWm3Ex7i6Mk0=
|
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 h1:ZTovoKIyXiK5VFRTVrY6YWHIFR5x98u9Q+k9rMjZzvg=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20210102085253-a691813b9041/go.mod h1:KL3i+ER58BrJ8JBkpy6WQ0mDZdlkgz7KWm3Ex7i6Mk0=
|
github.com/diamondburned/cchat-discord v0.0.0-20210102085253-a691813b9041/go.mod h1:KL3i+ER58BrJ8JBkpy6WQ0mDZdlkgz7KWm3Ex7i6Mk0=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210103044430-14970d0e05eb h1:IAitorXxVndnBGEW5uLcjjRLlpNBRPsUlwg9bUfyZLo=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210103044430-14970d0e05eb/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=
|
||||||
|
@ -104,6 +106,8 @@ github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385 h1:nmlMCeEWmT6
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20201230071527-a77c32eb3876 h1:8Hacuan9iBgoYyjJFYOP/TgghY4QWxT70qoV5NFGKmc=
|
github.com/diamondburned/gotk3 v0.0.0-20201230071527-a77c32eb3876 h1:8Hacuan9iBgoYyjJFYOP/TgghY4QWxT70qoV5NFGKmc=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20201230071527-a77c32eb3876/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
github.com/diamondburned/gotk3 v0.0.0-20201230071527-a77c32eb3876/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20210103063209-2bfa1d9dd9a6 h1:dJwNpjlgUP3YTPll5LvqyhpuaBPjITUuDes7Wq8HrTM=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20210103063209-2bfa1d9dd9a6/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894 h1:QgI21deaQbCUMnxKkQQUXzQolnAe1dMIXAWwqAyOp2g=
|
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894 h1:QgI21deaQbCUMnxKkQQUXzQolnAe1dMIXAWwqAyOp2g=
|
||||||
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894/go.mod h1:IoyMxPKSJOMoP0BiBuFwf2RDMeA4Uqx0HPKN5BzqTtA=
|
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894/go.mod h1:IoyMxPKSJOMoP0BiBuFwf2RDMeA4Uqx0HPKN5BzqTtA=
|
||||||
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 h1:Lm1F+GwrDdAaaMzrR7AYl4GGd/T+FE2OgOz25QWwsIg=
|
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 h1:Lm1F+GwrDdAaaMzrR7AYl4GGd/T+FE2OgOz25QWwsIg=
|
||||||
|
|
|
@ -222,6 +222,11 @@ func EventIsRightClick(ev *gdk.Event) bool {
|
||||||
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
|
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EventIsLeftClick(ev *gdk.Event) bool {
|
||||||
|
keyev := gdk.EventButtonNewFromEvent(ev)
|
||||||
|
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_PRIMARY
|
||||||
|
}
|
||||||
|
|
||||||
func SpawnUploader(dirpath string, callback func(absolutePaths []string)) {
|
func SpawnUploader(dirpath string, callback func(absolutePaths []string)) {
|
||||||
dialog, _ := gtk.FileChooserNativeDialogNew(
|
dialog, _ := gtk.FileChooserNativeDialogNew(
|
||||||
"Upload File", App.Window,
|
"Upload File", App.Window,
|
||||||
|
|
|
@ -4,14 +4,16 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"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/menu"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
|
||||||
|
"github.com/diamondburned/handy"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BacklogLimit is the maximum number of messages to store in the container at
|
// BacklogLimit is the maximum number of messages to store in the container at
|
||||||
// once.
|
// once.
|
||||||
const BacklogLimit = 35
|
const BacklogLimit = 50
|
||||||
|
|
||||||
type MessageRow interface {
|
type MessageRow interface {
|
||||||
message.Container
|
message.Container
|
||||||
|
@ -19,6 +21,8 @@ type MessageRow interface {
|
||||||
Row() *gtk.ListBoxRow
|
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
|
||||||
|
// MenuItems returns the list of attached menu items.
|
||||||
|
MenuItems() []menu.Item
|
||||||
// SetReferenceHighlighter sets the reference highlighter into the message.
|
// SetReferenceHighlighter sets the reference highlighter into the message.
|
||||||
SetReferenceHighlighter(refer labeluri.ReferenceHighlighter)
|
SetReferenceHighlighter(refer labeluri.ReferenceHighlighter)
|
||||||
}
|
}
|
||||||
|
@ -63,6 +67,8 @@ type Container interface {
|
||||||
|
|
||||||
// Controller is for menu actions.
|
// Controller is for menu actions.
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
|
// Connector is used for button press events to unselect messages.
|
||||||
|
primitives.Connector
|
||||||
// BindMenu expects the controller to add actioner into the message.
|
// BindMenu expects the controller to add actioner into the message.
|
||||||
BindMenu(MessageRow)
|
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.
|
||||||
|
@ -70,6 +76,10 @@ type Controller interface {
|
||||||
// AuthorEvent is called on message create/update. This is used to update
|
// AuthorEvent is called on message create/update. This is used to update
|
||||||
// the typer state.
|
// the typer state.
|
||||||
AuthorEvent(a cchat.Author)
|
AuthorEvent(a cchat.Author)
|
||||||
|
// SelectMessage is called when a message is selected.
|
||||||
|
SelectMessage(list *ListStore, msg MessageRow)
|
||||||
|
// UnselectMessage is called when the message selection is cleared.
|
||||||
|
UnselectMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor is an interface for making custom message implementations which
|
// Constructor is an interface for making custom message implementations which
|
||||||
|
@ -84,7 +94,10 @@ const ColumnSpacing = 8
|
||||||
// ListContainer is an implementation of Container, which allows flexible
|
// ListContainer is an implementation of Container, which allows flexible
|
||||||
// message grids.
|
// message grids.
|
||||||
type ListContainer struct {
|
type ListContainer struct {
|
||||||
|
*handy.Clamp
|
||||||
|
|
||||||
*ListStore
|
*ListStore
|
||||||
|
|
||||||
Controller
|
Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,8 +110,20 @@ type messageRow struct {
|
||||||
var _ Container = (*ListContainer)(nil)
|
var _ Container = (*ListContainer)(nil)
|
||||||
|
|
||||||
func NewListContainer(constr Constructor, ctrl Controller) *ListContainer {
|
func NewListContainer(constr Constructor, ctrl Controller) *ListContainer {
|
||||||
|
listStore := NewListStore(constr, ctrl)
|
||||||
|
listStore.ListBox.Show()
|
||||||
|
|
||||||
|
clamp := handy.ClampNew()
|
||||||
|
clamp.SetMaximumSize(800)
|
||||||
|
clamp.SetTighteningThreshold(600)
|
||||||
|
clamp.SetHExpand(true)
|
||||||
|
clamp.SetVExpand(true)
|
||||||
|
clamp.Add(listStore.ListBox)
|
||||||
|
clamp.Show()
|
||||||
|
|
||||||
return &ListContainer{
|
return &ListContainer{
|
||||||
ListStore: NewListStore(constr, ctrl),
|
Clamp: clamp,
|
||||||
|
ListStore: listStore,
|
||||||
Controller: ctrl,
|
Controller: ctrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,3 +148,10 @@ func (c *ListContainer) CleanMessages() bool {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ListContainer) SetFocusHAdjustment(adj *gtk.Adjustment) {
|
||||||
|
c.ListBox.SetFocusHAdjustment(adj)
|
||||||
|
}
|
||||||
|
func (c *ListContainer) SetFocusVAdjustment(adj *gtk.Adjustment) {
|
||||||
|
c.ListBox.SetFocusVAdjustment(adj)
|
||||||
|
}
|
||||||
|
|
|
@ -48,8 +48,6 @@ type Container struct {
|
||||||
func NewContainer(ctrl container.Controller) *Container {
|
func NewContainer(ctrl container.Controller) *Container {
|
||||||
c := &Container{}
|
c := &Container{}
|
||||||
c.ListContainer = container.NewListContainer(c, ctrl)
|
c.ListContainer = container.NewListContainer(c, ctrl)
|
||||||
// A not-so-generous row padding, as we will rely on margins per widget.
|
|
||||||
// c.ListContainer.Grid.SetRowSpacing(4)
|
|
||||||
|
|
||||||
primitives.AddClass(c, "cozy-container")
|
primitives.AddClass(c, "cozy-container")
|
||||||
return c
|
return c
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -26,57 +25,62 @@ var messageListCSS = primitives.PrepareClassCSS("message-list", `
|
||||||
`)
|
`)
|
||||||
|
|
||||||
type ListStore struct {
|
type ListStore struct {
|
||||||
*gtk.ListBox
|
ListBox *gtk.ListBox
|
||||||
|
|
||||||
Construct Constructor
|
Construct Constructor
|
||||||
Controller Controller
|
Controller Controller
|
||||||
|
|
||||||
resetMe bool
|
resetMe bool
|
||||||
|
|
||||||
messages map[messageKey]*messageRow
|
messages map[messageKey]*messageRow
|
||||||
messageList *list.List
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListStore(constr Constructor, ctrl Controller) *ListStore {
|
func NewListStore(constr Constructor, ctrl Controller) *ListStore {
|
||||||
listBox, _ := gtk.ListBoxNew()
|
listBox, _ := gtk.ListBoxNew()
|
||||||
listBox.SetSelectionMode(gtk.SELECTION_NONE)
|
listBox.SetSelectionMode(gtk.SELECTION_SINGLE)
|
||||||
listBox.Show()
|
listBox.Show()
|
||||||
messageListCSS(listBox)
|
messageListCSS(listBox)
|
||||||
|
|
||||||
return &ListStore{
|
listStore := ListStore{
|
||||||
ListBox: listBox,
|
ListBox: listBox,
|
||||||
Construct: constr,
|
Construct: constr,
|
||||||
Controller: ctrl,
|
Controller: ctrl,
|
||||||
messages: make(map[messageKey]*messageRow, BacklogLimit+1),
|
messages: make(map[messageKey]*messageRow, BacklogLimit+1),
|
||||||
messageList: list.New(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var selected bool
|
||||||
|
|
||||||
|
listBox.Connect("row-selected", func(listBox *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||||
|
if r == nil || selected {
|
||||||
|
if selected {
|
||||||
|
listBox.UnselectAll()
|
||||||
|
selected = false
|
||||||
|
}
|
||||||
|
ctrl.UnselectMessage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _ := r.GetName()
|
||||||
|
|
||||||
|
msg := listStore.Message(id, "")
|
||||||
|
if msg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selected = true
|
||||||
|
ctrl.SelectMessage(&listStore, msg)
|
||||||
|
})
|
||||||
|
|
||||||
|
return &listStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) Reset() {
|
func (c *ListStore) Reset() {
|
||||||
primitives.RemoveChildren(c.ListBox)
|
primitives.RemoveChildren(c.ListBox)
|
||||||
c.messages = make(map[messageKey]*messageRow, BacklogLimit+1)
|
c.messages = make(map[messageKey]*messageRow, BacklogLimit+1)
|
||||||
c.messageList = list.New()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) MessagesLen() int {
|
func (c *ListStore) MessagesLen() int {
|
||||||
return c.messageList.Len()
|
return len(c.messages)
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ListStore) findElement(id cchat.ID) (*list.Element, *messageRow, int) {
|
|
||||||
var index = c.messageList.Len() - 1
|
|
||||||
for elem := c.messageList.Back(); elem != nil; elem = elem.Prev() {
|
|
||||||
if gridMsg := elem.Value.(*messageRow); gridMsg.ID() == id {
|
|
||||||
return elem, gridMsg, index
|
|
||||||
}
|
|
||||||
index--
|
|
||||||
}
|
|
||||||
return nil, nil, -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// findIndex searches backwards for id.
|
|
||||||
func (c *ListStore) findIndex(id cchat.ID) (*messageRow, int) {
|
|
||||||
_, gridMsg, ix := c.findElement(id)
|
|
||||||
return gridMsg, ix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -124,33 +128,37 @@ func (c *ListStore) Around(id cchat.ID) (before, after MessageRow) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) around(id cchat.ID) (before, after *messageRow) {
|
func (c *ListStore) around(aroundID cchat.ID) (before, after *messageRow) {
|
||||||
var last *messageRow
|
var last *messageRow
|
||||||
var next bool
|
var next bool
|
||||||
|
|
||||||
for elem := c.messageList.Front(); elem != nil; elem = elem.Next() {
|
primitives.ForeachChildBackwards(c.ListBox, func(v interface{}) (stop bool) {
|
||||||
message := elem.Value.(*messageRow)
|
id := primitives.GetName(v.(primitives.Namer))
|
||||||
if next {
|
if next {
|
||||||
after = message
|
after = c.message(id, "")
|
||||||
break
|
return true
|
||||||
}
|
}
|
||||||
if message.ID() == id {
|
if id == aroundID {
|
||||||
// The last message is the before.
|
|
||||||
before = last
|
before = last
|
||||||
next = true
|
next = true
|
||||||
continue
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
last = message
|
last = c.message(id, "")
|
||||||
}
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestMessageFrom returns the latest message with the given user ID. This is
|
// LatestMessageFrom returns the latest message with the given user ID. This is
|
||||||
// used for the input prompt.
|
// used for the input prompt.
|
||||||
func (c *ListStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
func (c *ListStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
||||||
|
log.Println("LatestMessageFrom called")
|
||||||
|
|
||||||
// FindMessage already looks from the latest messages.
|
// FindMessage already looks from the latest messages.
|
||||||
var msg = c.FindMessage(func(msg MessageRow) bool {
|
var msg = c.FindMessage(func(msg MessageRow) bool {
|
||||||
|
log.Println("Author:", msg.AuthorName())
|
||||||
return msg.AuthorID() == userID
|
return msg.AuthorID() == userID
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -161,52 +169,95 @@ func (c *ListStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
||||||
return msg.ID(), true
|
return msg.ID(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindMessage iterates backwards and returns the message if isMessage() returns
|
// findIndex searches backwards for id.
|
||||||
// true on that message.
|
func (c *ListStore) findIndex(findID cchat.ID) (found *messageRow, index int) {
|
||||||
func (c *ListStore) FindMessage(isMessage func(msg MessageRow) bool) MessageRow {
|
// Faster implementation of findMessage: no map lookup is done until an ID
|
||||||
for elem := c.messageList.Back(); elem != nil; elem = elem.Prev() {
|
// match, so the worst case is a single string hash.
|
||||||
gridMsg := elem.Value.(*messageRow)
|
index = c.MessagesLen() - 1
|
||||||
// Ignore sending messages.
|
|
||||||
if gridMsg.presend != nil {
|
primitives.ForeachChildBackwards(c.ListBox, func(v interface{}) (stop bool) {
|
||||||
continue
|
id := primitives.GetName(v.(primitives.Namer))
|
||||||
}
|
if id == findID {
|
||||||
if gridMsg := gridMsg.MessageRow; isMessage(gridMsg) {
|
found = c.message(findID, "")
|
||||||
return gridMsg
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
index--
|
||||||
|
return index == 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Preserve old behavior.
|
||||||
|
if found == nil {
|
||||||
|
index = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int) {
|
||||||
|
var r *messageRow
|
||||||
|
var i = c.MessagesLen() - 1
|
||||||
|
|
||||||
|
primitives.ForeachChildBackwards(c.ListBox, func(v interface{}) (stop bool) {
|
||||||
|
id := primitives.GetName(v.(primitives.Namer))
|
||||||
|
gridMsg := c.message(id, "")
|
||||||
|
|
||||||
|
// If gridMsg is actually nil, then we have bigger issues.
|
||||||
|
if gridMsg != nil {
|
||||||
|
// Ignore sending messages.
|
||||||
|
if (presend || gridMsg.presend == nil) && fn(gridMsg) {
|
||||||
|
r = gridMsg
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i--
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Preserve old behavior.
|
||||||
|
if r == nil {
|
||||||
|
i = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, i
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindMessage iterates backwards and returns the message if isMessage() returns
|
||||||
|
// true on that message. It does not search presend messages.
|
||||||
|
func (c *ListStore) FindMessage(isMessage func(MessageRow) bool) MessageRow {
|
||||||
|
msg, _ := c.findMessage(false, func(row *messageRow) bool {
|
||||||
|
return isMessage(row.MessageRow)
|
||||||
|
})
|
||||||
|
if msg != nil {
|
||||||
|
return msg.MessageRow
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ListStore) nthMessage(n int) *messageRow {
|
||||||
|
v := primitives.NthChild(c.ListBox, n)
|
||||||
|
id := primitives.GetName(v.(primitives.Namer))
|
||||||
|
return c.message(id, "")
|
||||||
|
}
|
||||||
|
|
||||||
// NthMessage returns the nth message.
|
// NthMessage returns the nth message.
|
||||||
func (c *ListStore) NthMessage(n int) MessageRow {
|
func (c *ListStore) NthMessage(n int) MessageRow {
|
||||||
var index = 0
|
msg := c.nthMessage(n)
|
||||||
for elem := c.messageList.Front(); elem != nil; elem = elem.Next() {
|
if msg != nil {
|
||||||
if index == n {
|
return msg.MessageRow
|
||||||
return elem.Value.(*messageRow).MessageRow
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FirstMessage returns the first message.
|
// FirstMessage returns the first message.
|
||||||
func (c *ListStore) FirstMessage() MessageRow {
|
func (c *ListStore) FirstMessage() MessageRow {
|
||||||
if c.messageList.Len() == 0 {
|
return c.NthMessage(0)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Long unwrap.
|
|
||||||
return c.messageList.Front().Value.(*messageRow).MessageRow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LastMessage returns the latest message.
|
// LastMessage returns the latest message.
|
||||||
func (c *ListStore) LastMessage() MessageRow {
|
func (c *ListStore) LastMessage() MessageRow {
|
||||||
if c.messageList.Len() == 0 {
|
return c.NthMessage(c.MessagesLen() - 1)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Long unwrap.
|
|
||||||
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
|
||||||
|
@ -258,8 +309,6 @@ func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRo
|
||||||
|
|
||||||
// Set the message into the list.
|
// Set the message into the list.
|
||||||
c.ListBox.Insert(msgc.Row(), c.MessagesLen())
|
c.ListBox.Insert(msgc.Row(), c.MessagesLen())
|
||||||
// Append the message.
|
|
||||||
c.messageList.PushBack(msgc)
|
|
||||||
// Set the NONCE into the message map.
|
// Set the NONCE into the message map.
|
||||||
c.messages[nonceKey(msgc.Nonce())] = msgc
|
c.messages[nonceKey(msgc.Nonce())] = msgc
|
||||||
|
|
||||||
|
@ -267,6 +316,8 @@ func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) bindMessage(msgc *messageRow) {
|
func (c *ListStore) bindMessage(msgc *messageRow) {
|
||||||
|
// Bind the message ID to the row so we can easily do a lookup.
|
||||||
|
msgc.Row().SetName(msgc.ID())
|
||||||
msgc.SetReferenceHighlighter(c)
|
msgc.SetReferenceHighlighter(c)
|
||||||
c.Controller.BindMenu(msgc.MessageRow)
|
c.Controller.BindMenu(msgc.MessageRow)
|
||||||
}
|
}
|
||||||
|
@ -294,31 +345,21 @@ func (c *ListStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
}
|
}
|
||||||
msgTime := msg.Time()
|
msgTime := msg.Time()
|
||||||
|
|
||||||
var index = c.messageList.Len() - 1
|
|
||||||
var after = c.messageList.Back()
|
|
||||||
|
|
||||||
// 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 {
|
after, index := c.findMessage(true, func(after *messageRow) bool {
|
||||||
if msgTime.After(after.Value.(*messageRow).Time()) {
|
return msgTime.After(after.Time())
|
||||||
break
|
})
|
||||||
}
|
|
||||||
index--
|
|
||||||
after = after.Prev()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the message. If after is nil, then that means the message is the
|
// Append the message. If after is nil, then that means the message is the
|
||||||
// oldest, so we add it to the front of the list.
|
// oldest, so we add it to the front of the list.
|
||||||
if after != nil {
|
if after != nil {
|
||||||
index++ // insert right after
|
index++ // insert right after
|
||||||
c.messageList.InsertAfter(msgc, after)
|
c.ListBox.Insert(msgc.Row(), index)
|
||||||
} else {
|
} else {
|
||||||
index = 0
|
index = 0
|
||||||
c.messageList.PushFront(msgc)
|
c.ListBox.Add(msgc.Row())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the message into the grid.
|
|
||||||
c.ListBox.Insert(msgc.Row(), index)
|
|
||||||
|
|
||||||
// Set the ID into the message map.
|
// Set the ID into the message map.
|
||||||
c.messages[idKey(msgc.ID())] = msgc
|
c.messages[idKey(msgc.ID())] = msgc
|
||||||
|
|
||||||
|
@ -348,16 +389,14 @@ func (c *ListStore) DeleteMessageUnsafe(msg cchat.MessageDelete) {
|
||||||
// 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 *ListStore) PopMessage(id cchat.ID) (msg MessageRow) {
|
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, _ := c.findElement(id)
|
gridMsg, _ := c.findIndex(id)
|
||||||
if elem == nil {
|
if gridMsg == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
msg = gridMsg.MessageRow
|
msg = gridMsg.MessageRow
|
||||||
|
|
||||||
// Remove off of the Gtk grid.
|
// Remove off of the Gtk grid.
|
||||||
gridMsg.Row().Destroy()
|
gridMsg.Row().Destroy()
|
||||||
// Pop off the slice.
|
|
||||||
c.messageList.Remove(elem)
|
|
||||||
// Delete off the map.
|
// Delete off the map.
|
||||||
delete(c.messages, idKey(id))
|
delete(c.messages, idKey(id))
|
||||||
|
|
||||||
|
@ -373,8 +412,11 @@ func (c *ListStore) 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-- {
|
primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) {
|
||||||
gridMsg := elem.Value.(*messageRow)
|
id := primitives.GetName(v.(primitives.Namer))
|
||||||
|
gridMsg := c.message(id, "")
|
||||||
|
|
||||||
|
log.Println("Deleting overflowed message ID from", gridMsg.AuthorName())
|
||||||
|
|
||||||
if id := gridMsg.ID(); id != "" {
|
if id := gridMsg.ID(); id != "" {
|
||||||
delete(c.messages, idKey(id))
|
delete(c.messages, idKey(id))
|
||||||
|
@ -385,15 +427,13 @@ func (c *ListStore) DeleteEarliest(n int) {
|
||||||
|
|
||||||
gridMsg.Row().Destroy()
|
gridMsg.Row().Destroy()
|
||||||
|
|
||||||
next := elem.Next()
|
n--
|
||||||
c.messageList.Remove(elem)
|
return n == 0
|
||||||
elem = next
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) HighlightReference(ref markup.ReferenceSegment) {
|
func (c *ListStore) HighlightReference(ref markup.ReferenceSegment) {
|
||||||
msg := c.message(ref.MessageID(), "")
|
msg := c.message(ref.MessageID(), "")
|
||||||
log.Println("Highlighting", ref.MessageID())
|
|
||||||
if msg != nil {
|
if msg != nil {
|
||||||
c.Highlight(msg)
|
c.Highlight(msg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,15 @@ import (
|
||||||
// const BreadcrumbSlash = `<span rise="-1024" size="x-large">❭</span>`
|
// const BreadcrumbSlash = `<span rise="-1024" size="x-large">❭</span>`
|
||||||
const BreadcrumbSlash = " 〉"
|
const BreadcrumbSlash = " 〉"
|
||||||
|
|
||||||
|
const iconSize = gtk.ICON_SIZE_BUTTON
|
||||||
|
|
||||||
type Header struct {
|
type Header struct {
|
||||||
handy.HeaderBar
|
handy.HeaderBar
|
||||||
|
|
||||||
ShowBackBtn *gtk.Revealer
|
ShowBackBtn *gtk.Revealer
|
||||||
BackButton *gtk.Button
|
BackButton *gtk.Button
|
||||||
Breadcrumb *gtk.Label
|
Breadcrumb *gtk.Label
|
||||||
|
MessageCtrl *MessageControl
|
||||||
ShowMembers *gtk.ToggleButton
|
ShowMembers *gtk.ToggleButton
|
||||||
|
|
||||||
breadcrumbs []string
|
breadcrumbs []string
|
||||||
|
@ -39,7 +42,7 @@ var rightBreadcrumbCSS = primitives.PrepareClassCSS("right-breadcrumb", `
|
||||||
`)
|
`)
|
||||||
|
|
||||||
func NewHeader() *Header {
|
func NewHeader() *Header {
|
||||||
bk, _ := gtk.ButtonNewFromIconName("go-previous-symbolic", gtk.ICON_SIZE_BUTTON)
|
bk, _ := gtk.ButtonNewFromIconName("go-previous-symbolic", iconSize)
|
||||||
bk.SetVAlign(gtk.ALIGN_CENTER)
|
bk.SetVAlign(gtk.ALIGN_CENTER)
|
||||||
bk.Show()
|
bk.Show()
|
||||||
backButtonCSS(bk)
|
backButtonCSS(bk)
|
||||||
|
@ -61,7 +64,11 @@ func NewHeader() *Header {
|
||||||
bc.Show()
|
bc.Show()
|
||||||
rightBreadcrumbCSS(bc)
|
rightBreadcrumbCSS(bc)
|
||||||
|
|
||||||
memberIcon, _ := gtk.ImageNewFromIconName("system-users-symbolic", gtk.ICON_SIZE_BUTTON)
|
msgctrl := NewMessageControl()
|
||||||
|
msgctrl.Disable()
|
||||||
|
msgctrl.Show()
|
||||||
|
|
||||||
|
memberIcon, _ := gtk.ImageNewFromIconName("system-users-symbolic", iconSize)
|
||||||
memberIcon.Show()
|
memberIcon.Show()
|
||||||
|
|
||||||
mb, _ := gtk.ToggleButtonNew()
|
mb, _ := gtk.ToggleButtonNew()
|
||||||
|
@ -75,6 +82,7 @@ func NewHeader() *Header {
|
||||||
header.PackStart(rbk)
|
header.PackStart(rbk)
|
||||||
header.PackStart(bc)
|
header.PackStart(bc)
|
||||||
header.PackEnd(mb)
|
header.PackEnd(mb)
|
||||||
|
header.PackEnd(msgctrl)
|
||||||
header.Show()
|
header.Show()
|
||||||
|
|
||||||
// Hack to hide the title.
|
// Hack to hide the title.
|
||||||
|
@ -86,12 +94,14 @@ func NewHeader() *Header {
|
||||||
ShowBackBtn: rbk,
|
ShowBackBtn: rbk,
|
||||||
BackButton: bk,
|
BackButton: bk,
|
||||||
Breadcrumb: bc,
|
Breadcrumb: bc,
|
||||||
|
MessageCtrl: msgctrl,
|
||||||
ShowMembers: mb,
|
ShowMembers: mb,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Header) Reset() {
|
func (h *Header) Reset() {
|
||||||
h.SetBreadcrumber(nil)
|
h.SetBreadcrumber(nil)
|
||||||
|
h.MessageCtrl.Disable()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Header) OnBackPressed(fn func()) {
|
func (h *Header) OnBackPressed(fn func()) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||||
|
"github.com/diamondburned/handy"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -54,7 +55,7 @@ var textCSS = primitives.PrepareCSS(`
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
var inputBoxCSS = primitives.PrepareClassCSS("input-box", `
|
var inputMainBoxCSS = primitives.PrepareClassCSS("input-box", `
|
||||||
.input-box {
|
.input-box {
|
||||||
background-color: @theme_bg_color;
|
background-color: @theme_bg_color;
|
||||||
}
|
}
|
||||||
|
@ -111,14 +112,20 @@ const (
|
||||||
editButtonIcon = "document-edit-symbolic"
|
editButtonIcon = "document-edit-symbolic"
|
||||||
replyButtonIcon = "mail-reply-sender-symbolic"
|
replyButtonIcon = "mail-reply-sender-symbolic"
|
||||||
sendButtonSize = gtk.ICON_SIZE_BUTTON
|
sendButtonSize = gtk.ICON_SIZE_BUTTON
|
||||||
|
|
||||||
|
ClampMaxSize = 1000
|
||||||
|
ClampThreshold = ClampMaxSize
|
||||||
)
|
)
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
// Box contains the field box and the attachment container.
|
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
|
Clamp *handy.Clamp
|
||||||
|
|
||||||
|
// MainBox contains the field box and the attachment container.
|
||||||
|
MainBox *gtk.Box
|
||||||
Attachments *attachment.Container
|
Attachments *attachment.Container
|
||||||
|
|
||||||
// FieldBox contains the username container and the input field. It spans
|
// FieldMainBox contains the username container and the input field. It spans
|
||||||
// horizontally.
|
// horizontally.
|
||||||
FieldBox *gtk.Box
|
FieldBox *gtk.Box
|
||||||
Username *username.Container
|
Username *username.Container
|
||||||
|
@ -212,11 +219,22 @@ func NewField(text *gtk.TextView, ctrl Controller, labeler LabelBorrower) *Field
|
||||||
field.Attachments = attachment.New()
|
field.Attachments = attachment.New()
|
||||||
field.Attachments.Show()
|
field.Attachments.Show()
|
||||||
|
|
||||||
field.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
field.MainBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
||||||
field.Box.PackStart(field.Attachments, false, false, 0)
|
field.MainBox.PackStart(field.Attachments, false, false, 0)
|
||||||
field.Box.PackStart(field.FieldBox, false, false, 0)
|
field.MainBox.PackStart(field.FieldBox, false, false, 0)
|
||||||
|
field.MainBox.Show()
|
||||||
|
|
||||||
|
field.Clamp = handy.ClampNew()
|
||||||
|
field.Clamp.SetMaximumSize(ClampMaxSize)
|
||||||
|
field.Clamp.SetTighteningThreshold(ClampThreshold)
|
||||||
|
field.Clamp.SetHExpand(true)
|
||||||
|
field.Clamp.Add(field.MainBox)
|
||||||
|
field.Clamp.Show()
|
||||||
|
|
||||||
|
field.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
field.Box.Add(field.Clamp)
|
||||||
field.Box.Show()
|
field.Box.Show()
|
||||||
inputBoxCSS(field.Box)
|
inputMainBoxCSS(field.Clamp)
|
||||||
|
|
||||||
text.SetFocusHAdjustment(field.TextScroll.GetHAdjustment())
|
text.SetFocusHAdjustment(field.TextScroll.GetHAdjustment())
|
||||||
text.SetFocusVAdjustment(field.TextScroll.GetVAdjustment())
|
text.SetFocusVAdjustment(field.TextScroll.GetVAdjustment())
|
||||||
|
|
|
@ -67,7 +67,7 @@ type GenericContainer struct {
|
||||||
contentBox *gtk.Box // basically what is in Content
|
contentBox *gtk.Box // basically what is in Content
|
||||||
ContentBody *labeluri.Label
|
ContentBody *labeluri.Label
|
||||||
|
|
||||||
MenuItems []menu.Item
|
menuItems []menu.Item
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Container = (*GenericContainer)(nil)
|
var _ Container = (*GenericContainer)(nil)
|
||||||
|
@ -114,7 +114,7 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
|
|
||||||
ctbody := labeluri.NewLabel(text.Rich{})
|
ctbody := labeluri.NewLabel(text.Rich{})
|
||||||
ctbody.SetVExpand(true)
|
ctbody.SetVExpand(true)
|
||||||
ctbody.SetHExpand(true)
|
ctbody.SetHAlign(gtk.ALIGN_START)
|
||||||
ctbody.SetEllipsize(pango.ELLIPSIZE_NONE)
|
ctbody.SetEllipsize(pango.ELLIPSIZE_NONE)
|
||||||
ctbody.SetLineWrap(true)
|
ctbody.SetLineWrap(true)
|
||||||
ctbody.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
ctbody.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||||
|
@ -125,6 +125,7 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
|
|
||||||
// Wrap the content label inside a content box.
|
// Wrap the content label inside a content box.
|
||||||
ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
ctbox.SetHExpand(true)
|
||||||
ctbox.PackStart(ctbody, false, false, 0)
|
ctbox.PackStart(ctbody, false, false, 0)
|
||||||
ctbox.Show()
|
ctbox.Show()
|
||||||
|
|
||||||
|
@ -161,7 +162,7 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
// Bind the custom popup menu to the content label.
|
// Bind the custom popup menu to the content label.
|
||||||
gc.ContentBody.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) {
|
gc.ContentBody.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) {
|
||||||
menu.MenuSeparator(m)
|
menu.MenuSeparator(m)
|
||||||
menu.MenuItems(m, gc.MenuItems)
|
menu.MenuItems(m, gc.menuItems)
|
||||||
})
|
})
|
||||||
|
|
||||||
return gc
|
return gc
|
||||||
|
@ -248,7 +249,12 @@ func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
|
||||||
// AttachMenu connects signal handlers to handle a list of menu items from
|
// AttachMenu connects signal handlers to handle a list of menu items from
|
||||||
// the container.
|
// the container.
|
||||||
func (m *GenericContainer) AttachMenu(newItems []menu.Item) {
|
func (m *GenericContainer) AttachMenu(newItems []menu.Item) {
|
||||||
m.MenuItems = newItems
|
m.menuItems = newItems
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuItems returns the list of menu items for this message.
|
||||||
|
func (m *GenericContainer) MenuItems() []menu.Item {
|
||||||
|
return m.menuItems
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) Focusable() gtk.IWidget {
|
func (m *GenericContainer) Focusable() gtk.IWidget {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/humanize"
|
"github.com/diamondburned/cchat-gtk/internal/humanize"
|
||||||
"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/input/attachment"
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/attachment"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/gotk3/gotk3/pango"
|
"github.com/gotk3/gotk3/pango"
|
||||||
)
|
)
|
||||||
|
@ -136,7 +137,8 @@ func (m *GenericPresendContainer) SetSentError(err error) {
|
||||||
|
|
||||||
// clearBox clears everything inside the content container.
|
// clearBox clears everything inside the content container.
|
||||||
func (m *GenericPresendContainer) clearBox() {
|
func (m *GenericPresendContainer) clearBox() {
|
||||||
m.contentBox.GetChildren().Foreach(func(v interface{}) {
|
primitives.ForeachChild(m.contentBox, func(v interface{}) (stop bool) {
|
||||||
m.contentBox.Remove(v.(gtk.IWidget))
|
m.contentBox.Remove(v.(gtk.IWidget))
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
package messages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bindableButton struct {
|
||||||
|
gtk.Button
|
||||||
|
h glib.SignalHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBindableButton(iconName string) *bindableButton {
|
||||||
|
btn, _ := gtk.ButtonNewFromIconName(iconName, iconSize)
|
||||||
|
return &bindableButton{
|
||||||
|
Button: *btn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (btn *bindableButton) bind(fn func()) {
|
||||||
|
btn.unbind()
|
||||||
|
if fn != nil {
|
||||||
|
btn.h = btn.Connect("clicked", func(*gtk.Button) { fn() })
|
||||||
|
btn.SetSensitive(true)
|
||||||
|
btn.Show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (btn *bindableButton) unbind() {
|
||||||
|
if btn.h > 0 {
|
||||||
|
btn.HandlerDisconnect(btn.h)
|
||||||
|
btn.h = 0
|
||||||
|
btn.SetSensitive(false)
|
||||||
|
btn.Hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageItemNames contains names that MessageControl will use for its menu
|
||||||
|
// action callbacks.
|
||||||
|
type MessageItemNames struct {
|
||||||
|
Reply, Edit, Delete string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageControl controls buttons that control a selected message.
|
||||||
|
type MessageControl struct {
|
||||||
|
gtk.Revealer
|
||||||
|
Box *gtk.Box
|
||||||
|
|
||||||
|
hide bool
|
||||||
|
|
||||||
|
Reply *bindableButton
|
||||||
|
Edit *bindableButton
|
||||||
|
Delete *bindableButton // Actions "Delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessageControl() *MessageControl {
|
||||||
|
mc := MessageControl{}
|
||||||
|
|
||||||
|
mc.Reply = newBindableButton("mail-reply-sender-symbolic")
|
||||||
|
mc.Edit = newBindableButton("document-edit-symbolic")
|
||||||
|
mc.Delete = newBindableButton("edit-delete-symbolic")
|
||||||
|
|
||||||
|
mc.Box, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 2)
|
||||||
|
mc.Box.Add(mc.Reply)
|
||||||
|
mc.Box.Add(mc.Edit)
|
||||||
|
mc.Box.Add(mc.Delete)
|
||||||
|
mc.Box.Show()
|
||||||
|
|
||||||
|
r, _ := gtk.RevealerNew()
|
||||||
|
mc.Revealer = *r
|
||||||
|
mc.Revealer.SetTransitionDuration(75)
|
||||||
|
mc.Revealer.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE)
|
||||||
|
mc.Revealer.Add(mc.Box)
|
||||||
|
|
||||||
|
mc.Disable()
|
||||||
|
|
||||||
|
return &mc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable enables the MessageControl with the given message.
|
||||||
|
func (mc *MessageControl) Enable(msg container.MessageRow, names MessageItemNames) {
|
||||||
|
mc.SetSensitive(true)
|
||||||
|
mc.SetRevealChild(true && !mc.hide)
|
||||||
|
|
||||||
|
items := msg.MenuItems()
|
||||||
|
|
||||||
|
mc.Reply.bind(menu.FindItemFunc(items, names.Reply))
|
||||||
|
mc.Edit.bind(menu.FindItemFunc(items, names.Edit))
|
||||||
|
mc.Delete.bind(menu.FindItemFunc(items, names.Delete))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHidden sets whether or not the control should be hidden.
|
||||||
|
func (mc *MessageControl) SetHidden(hidden bool) {
|
||||||
|
mc.hide = hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable disables the MessageControl and hides it.
|
||||||
|
func (mc *MessageControl) Disable() {
|
||||||
|
mc.SetSensitive(false)
|
||||||
|
mc.SetRevealChild(false)
|
||||||
|
|
||||||
|
mc.Reply.unbind()
|
||||||
|
mc.Edit.unbind()
|
||||||
|
mc.Delete.unbind()
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package messages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerMessage combines Server and ServerMessage from cchat.
|
||||||
|
type ServerMessage interface {
|
||||||
|
cchat.Server
|
||||||
|
cchat.Messenger
|
||||||
|
}
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
session cchat.Session
|
||||||
|
server cchat.Server
|
||||||
|
|
||||||
|
actioner cchat.Actioner
|
||||||
|
backlogger cchat.Backlogger
|
||||||
|
|
||||||
|
current func() // stop callback
|
||||||
|
author string
|
||||||
|
|
||||||
|
lastBacklogged time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) Reset() {
|
||||||
|
// If we still have the last server to leave, then leave it.
|
||||||
|
if s.current != nil {
|
||||||
|
s.current()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy way to reset the state.
|
||||||
|
*s = state{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) hasActions() bool {
|
||||||
|
return s.actioner != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionID returns the session ID, or an empty string if there's no session.
|
||||||
|
func (s *state) SessionID() string {
|
||||||
|
if s.session != nil {
|
||||||
|
return s.session.ID()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerID returns the server ID, or an empty string if there's no server.
|
||||||
|
func (s *state) ServerID() string {
|
||||||
|
if s.server != nil {
|
||||||
|
return s.server.ID()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const backloggingFreq = time.Second * 3
|
||||||
|
|
||||||
|
// Backlogger returns the backlogger instance if it's allowed to fetch more
|
||||||
|
// backlogs.
|
||||||
|
func (s *state) Backlogger() cchat.Backlogger {
|
||||||
|
if s.backlogger == nil || s.current == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = time.Now()
|
||||||
|
|
||||||
|
if s.lastBacklogged.Add(backloggingFreq).After(now) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastBacklogged = now
|
||||||
|
return s.backlogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) bind(session cchat.Session, server cchat.Server, msgr cchat.Messenger) {
|
||||||
|
s.session = session
|
||||||
|
s.server = server
|
||||||
|
s.actioner = msgr.AsActioner()
|
||||||
|
s.backlogger = msgr.AsBacklogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) setcurrent(fn func()) {
|
||||||
|
s.current = fn
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"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/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||||
|
"github.com/diamondburned/handy"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/gotk3/gotk3/pango"
|
"github.com/gotk3/gotk3/pango"
|
||||||
)
|
)
|
||||||
|
@ -33,10 +34,17 @@ var smallfonts = primitives.PrepareCSS(`
|
||||||
* { font-size: 0.9em; }
|
* { font-size: 0.9em; }
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Keep the same as input.
|
||||||
|
ClampMaxSize = 1000 - 6*2 // account for margin
|
||||||
|
ClampThreshold = ClampMaxSize
|
||||||
|
)
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
*gtk.Revealer
|
*gtk.Revealer
|
||||||
state *State
|
state *State
|
||||||
|
|
||||||
|
clamp *handy.Clamp
|
||||||
dots *gtk.Box
|
dots *gtk.Box
|
||||||
label *gtk.Label
|
label *gtk.Label
|
||||||
|
|
||||||
|
@ -62,11 +70,18 @@ func New() *Container {
|
||||||
b.PackStart(l, true, true, 0)
|
b.PackStart(l, true, true, 0)
|
||||||
b.Show()
|
b.Show()
|
||||||
|
|
||||||
|
c := handy.ClampNew()
|
||||||
|
c.SetMaximumSize(ClampMaxSize)
|
||||||
|
c.SetTighteningThreshold(ClampThreshold)
|
||||||
|
c.SetHExpand(true)
|
||||||
|
c.Add(b)
|
||||||
|
c.Show()
|
||||||
|
|
||||||
r, _ := gtk.RevealerNew()
|
r, _ := gtk.RevealerNew()
|
||||||
r.SetTransitionDuration(100)
|
r.SetTransitionDuration(100)
|
||||||
r.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE)
|
r.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE)
|
||||||
r.SetRevealChild(false)
|
r.SetRevealChild(false)
|
||||||
r.Add(b)
|
r.Add(c)
|
||||||
|
|
||||||
typingIndicatorCSS(b)
|
typingIndicatorCSS(b)
|
||||||
|
|
||||||
|
|
|
@ -55,8 +55,8 @@ type Controller interface {
|
||||||
|
|
||||||
type MessagesContainer interface {
|
type MessagesContainer interface {
|
||||||
gtk.IWidget
|
gtk.IWidget
|
||||||
container.Container
|
|
||||||
cchat.MessagesContainer
|
cchat.MessagesContainer
|
||||||
|
container.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
type View struct {
|
type View struct {
|
||||||
|
@ -250,11 +250,8 @@ func (v *View) reset() {
|
||||||
|
|
||||||
func (v *View) SetFolded(folded bool) {
|
func (v *View) SetFolded(folded bool) {
|
||||||
v.parentFolded = folded
|
v.parentFolded = folded
|
||||||
|
|
||||||
// Change to a mini breadcrumb if we're collapsed.
|
|
||||||
v.Header.SetMiniBreadcrumb(folded)
|
v.Header.SetMiniBreadcrumb(folded)
|
||||||
|
v.Header.MessageCtrl.SetHidden(folded)
|
||||||
// Hide the username in the input bar if we're collapsed.
|
|
||||||
v.InputView.Username.SetRevealChild(!folded)
|
v.InputView.Username.SetRevealChild(!folded)
|
||||||
|
|
||||||
// Hide the member list automatically on folded.
|
// Hide the member list automatically on folded.
|
||||||
|
@ -266,7 +263,7 @@ func (v *View) SetFolded(folded bool) {
|
||||||
// MemberListUpdated is called everytime the member list is updated.
|
// MemberListUpdated is called everytime the member list is updated.
|
||||||
func (v *View) MemberListUpdated(c *memberlist.Container) {
|
func (v *View) MemberListUpdated(c *memberlist.Container) {
|
||||||
// We can show the members list if it's not empty.
|
// We can show the members list if it's not empty.
|
||||||
var empty = c.IsEmpty()
|
empty := c.IsEmpty()
|
||||||
v.Header.SetCanShowMembers(!empty)
|
v.Header.SetCanShowMembers(!empty)
|
||||||
|
|
||||||
// If the member list is now empty, then hide the entire thing.
|
// If the member list is now empty, then hide the entire thing.
|
||||||
|
@ -430,6 +427,12 @@ func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendM
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var messageItemNames = MessageItemNames{
|
||||||
|
Reply: "Reply",
|
||||||
|
Edit: "Edit",
|
||||||
|
Delete: "Delete",
|
||||||
|
}
|
||||||
|
|
||||||
// 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.MessageRow) {
|
func (v *View) BindMenu(msg container.MessageRow) {
|
||||||
|
@ -475,81 +478,13 @@ func (v *View) makeActionItem(action, msgID string) menu.Item {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerMessage combines Server and ServerMessage from cchat.
|
// SelectMessage is called when a message is selected.
|
||||||
type ServerMessage interface {
|
func (v *View) SelectMessage(_ *container.ListStore, msg container.MessageRow) {
|
||||||
cchat.Server
|
// Hijack the message's action list to search for what we have above.
|
||||||
cchat.Messenger
|
v.Header.MessageCtrl.Enable(msg, messageItemNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
type state struct {
|
// UnselectMessage is called when the message selection is cleared.
|
||||||
session cchat.Session
|
func (v *View) UnselectMessage() {
|
||||||
server cchat.Server
|
v.Header.MessageCtrl.Disable()
|
||||||
|
|
||||||
actioner cchat.Actioner
|
|
||||||
backlogger cchat.Backlogger
|
|
||||||
|
|
||||||
current func() // stop callback
|
|
||||||
author string
|
|
||||||
|
|
||||||
lastBacklogged time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *state) Reset() {
|
|
||||||
// If we still have the last server to leave, then leave it.
|
|
||||||
if s.current != nil {
|
|
||||||
s.current()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lazy way to reset the state.
|
|
||||||
*s = state{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *state) hasActions() bool {
|
|
||||||
return s.actioner != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SessionID returns the session ID, or an empty string if there's no session.
|
|
||||||
func (s *state) SessionID() string {
|
|
||||||
if s.session != nil {
|
|
||||||
return s.session.ID()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerID returns the server ID, or an empty string if there's no server.
|
|
||||||
func (s *state) ServerID() string {
|
|
||||||
if s.server != nil {
|
|
||||||
return s.server.ID()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const backloggingFreq = time.Second * 3
|
|
||||||
|
|
||||||
// Backlogger returns the backlogger instance if it's allowed to fetch more
|
|
||||||
// backlogs.
|
|
||||||
func (s *state) Backlogger() cchat.Backlogger {
|
|
||||||
if s.backlogger == nil || s.current == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = time.Now()
|
|
||||||
|
|
||||||
if s.lastBacklogged.Add(backloggingFreq).After(now) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s.lastBacklogged = now
|
|
||||||
return s.backlogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *state) bind(session cchat.Session, server cchat.Server, msgr cchat.Messenger) {
|
|
||||||
s.session = session
|
|
||||||
s.server = server
|
|
||||||
s.actioner = msgr.AsActioner()
|
|
||||||
s.backlogger = msgr.AsBacklogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *state) setcurrent(fn func()) {
|
|
||||||
s.current = fn
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,14 +110,7 @@ func (c *Completer) Popdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Completer) Clear() {
|
func (c *Completer) Clear() {
|
||||||
var children = c.List.GetChildren()
|
primitives.RemoveChildren(c.List)
|
||||||
if children.Length() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
children.Foreach(func(i interface{}) {
|
|
||||||
i.(primitives.WidgetDestroyer).Destroy()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Words returns the buffer content split into words.
|
// Words returns the buffer content split into words.
|
||||||
|
|
|
@ -32,7 +32,7 @@ type KeyDownHandlerFn = func(gtk.IWidget, *gdk.Event) bool
|
||||||
func KeyDownHandler(l *gtk.ListBox, focus func()) KeyDownHandlerFn {
|
func KeyDownHandler(l *gtk.ListBox, focus func()) KeyDownHandlerFn {
|
||||||
return func(w gtk.IWidget, ev *gdk.Event) bool {
|
return func(w gtk.IWidget, ev *gdk.Event) bool {
|
||||||
// Do we have any entries? If not, don't bother.
|
// Do we have any entries? If not, don't bother.
|
||||||
var length = int(l.GetChildren().Length())
|
var length = primitives.ChildrenLen(l)
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
// passthrough.
|
// passthrough.
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -72,6 +72,17 @@ func MenuItems(menu MenuAppender, items []Item) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindItemFunc iterates over the list of items and returns the first item with
|
||||||
|
// the matching name.
|
||||||
|
func FindItemFunc(items []Item, name string) func() {
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Name == name {
|
||||||
|
return item.Func
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ToolbarInserter interface {
|
type ToolbarInserter interface {
|
||||||
Insert(gtk.IToolItem, int)
|
Insert(gtk.IToolItem, int)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,9 +30,53 @@ func RemoveChildren(w Container) {
|
||||||
Destroy()
|
Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
w.GetChildren().Foreach(func(child interface{}) {
|
children := w.GetChildren()
|
||||||
child.(destroyer).Destroy()
|
children.Foreach(func(child interface{}) { w.Remove(child.(gtk.IWidget)) })
|
||||||
})
|
children.Free()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChildrenLen gets the total count of children for the given container.
|
||||||
|
func ChildrenLen(w Container) int {
|
||||||
|
children := w.GetChildren()
|
||||||
|
defer children.Free()
|
||||||
|
|
||||||
|
return int(children.Length())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NthChild(w Container, n int) interface{} {
|
||||||
|
children := w.GetChildren()
|
||||||
|
defer children.Free()
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
return children.Data()
|
||||||
|
}
|
||||||
|
return children.NthData(uint(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForeachChildBackwards iterates the list. If the callback returns true, then
|
||||||
|
// the loop is broken.
|
||||||
|
func ForeachChild(w Container, fn func(interface{}) (stop bool)) {
|
||||||
|
children := w.GetChildren()
|
||||||
|
defer children.Free()
|
||||||
|
|
||||||
|
for v := children; v != nil; v = v.Next() {
|
||||||
|
if fn(v.Data()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForeachChildBackwards iterates the list backwards. If the callback returns
|
||||||
|
// true, then the loop is broken.
|
||||||
|
func ForeachChildBackwards(w Container, fn func(interface{}) (stop bool)) {
|
||||||
|
children := w.GetChildren()
|
||||||
|
defer children.Free()
|
||||||
|
|
||||||
|
for v := children.Last(); v != nil; v = v.Previous() {
|
||||||
|
if fn(v.Data()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Namer interface {
|
type Namer interface {
|
||||||
|
|
|
@ -27,8 +27,8 @@ import (
|
||||||
const (
|
const (
|
||||||
AvatarSize = 96
|
AvatarSize = 96
|
||||||
PopoverWidth = 250
|
PopoverWidth = 250
|
||||||
MaxWidth = 350
|
MaxWidth = 500
|
||||||
MaxHeight = 350
|
MaxHeight = 500
|
||||||
)
|
)
|
||||||
|
|
||||||
type WidgetConnector interface {
|
type WidgetConnector interface {
|
||||||
|
|
|
@ -117,7 +117,7 @@ func (a AppendMap) Get(ind int) (tags string) {
|
||||||
|
|
||||||
// Borrowing appended's backing array to add prepended is probably fine, as
|
// Borrowing appended's backing array to add prepended is probably fine, as
|
||||||
// the length of the actual appended slice is going to stay the same.
|
// the length of the actual appended slice is going to stay the same.
|
||||||
return string(append(appended, prepended...))
|
return string(append(prepended, appended...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppendMap) Finalize(strlen int) []int {
|
func (a *AppendMap) Finalize(strlen int) []int {
|
||||||
|
|
|
@ -262,6 +262,11 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
|
||||||
var lastIndex = 0
|
var lastIndex = 0
|
||||||
|
|
||||||
for _, index := range appended.Finalize(len(content.Content)) {
|
for _, index := range appended.Finalize(len(content.Content)) {
|
||||||
|
// Prevent faulty backend with erroneous insertions.
|
||||||
|
if index > len(content.Content) {
|
||||||
|
index = len(content.Content)
|
||||||
|
}
|
||||||
|
|
||||||
// Write the content.
|
// Write the content.
|
||||||
buf.WriteString(html.EscapeString(content.Content[lastIndex:index]))
|
buf.WriteString(html.EscapeString(content.Content[lastIndex:index]))
|
||||||
// Write the tags.
|
// Write the tags.
|
||||||
|
|
|
@ -36,7 +36,7 @@ func Restore(conf Configurator) {
|
||||||
|
|
||||||
file := serviceFile(conf)
|
file := serviceFile(conf)
|
||||||
|
|
||||||
if err := config.UnmarshalFromFile(file, c); err != nil {
|
if err := config.UnmarshalFromFile(file, &c); err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to unmarshal %s config", conf.Name())
|
return nil, errors.Wrapf(err, "failed to unmarshal %s config", conf.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ func Spawn(conf Configurator) error {
|
||||||
|
|
||||||
file := serviceFile(conf)
|
file := serviceFile(conf)
|
||||||
|
|
||||||
err = config.UnmarshalFromFile(file, c)
|
err = config.UnmarshalFromFile(file, &c)
|
||||||
err = errors.Wrapf(err, "failed to unmarshal %s config", conf.Name())
|
err = errors.Wrapf(err, "failed to unmarshal %s config", conf.Name())
|
||||||
|
|
||||||
return func() {
|
return func() {
|
||||||
|
|
|
@ -114,9 +114,13 @@ func (s *Servers) SetServers(servers []cchat.Server) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
s.Children.SetServersUnsafe(servers)
|
s.Children.SetServersUnsafe(servers)
|
||||||
|
|
||||||
if servers == nil {
|
if len(servers) == 0 {
|
||||||
s.ctrl.ClearMessenger()
|
s.ctrl.ClearMessenger()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload all top-level nodes.
|
||||||
|
s.Children.LoadAll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@ func (app *App) SessionSelected(svc *service.Service, ses *session.Row) {
|
||||||
|
|
||||||
func (app *App) ClearMessenger(ses *session.Row) {
|
func (app *App) ClearMessenger(ses *session.Row) {
|
||||||
if app.MessageView.SessionID() == ses.Session.ID() {
|
if app.MessageView.SessionID() == ses.Session.ID() {
|
||||||
return
|
app.MessageView.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue