Minor improvements

This commit is contained in:
diamondburned 2020-08-20 16:53:13 -07:00
parent af2fe85666
commit b44fa90c84
20 changed files with 426 additions and 103 deletions

4
go.mod
View File

@ -7,8 +7,8 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200816
require (
github.com/Xuanwo/go-locale v0.2.0
github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.0.48
github.com/diamondburned/cchat-discord v0.0.0-20200816234747-9647ad0709f0
github.com/diamondburned/cchat v0.0.49
github.com/diamondburned/cchat-discord v0.0.0-20200820222718-68cfafc4c318
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
github.com/disintegration/imaging v1.6.2

22
go.sum
View File

@ -48,6 +48,10 @@ github.com/diamondburned/arikawa v0.12.4 h1:lhWJqcGkIIMiOYWdsoEuGlri2UbMkzMeh+Vf
github.com/diamondburned/arikawa v0.12.4/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
github.com/diamondburned/arikawa v1.1.6 h1:Y/ioTYipS2v/NXfcAEhCnMTzrpxDjWlkjLKKcX29n6o=
github.com/diamondburned/arikawa v1.1.6/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
github.com/diamondburned/arikawa v1.2.0 h1:3dFmpk/G4UwO+Kto0tXd5AbaCKC9KH2ZfnA8UOdzQ1k=
github.com/diamondburned/arikawa v1.2.0/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
github.com/diamondburned/arikawa v1.3.0 h1:up5q5Ya/QbiFqhMejvl+c03YdsgzkzspsJOWW30A2lk=
github.com/diamondburned/arikawa v1.3.0/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
github.com/diamondburned/cchat v0.0.43 h1:HetAujSaUSdnQgAUZgprNLARjf/MSWXpCfWdvX2wOCU=
github.com/diamondburned/cchat v0.0.43/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.45 h1:HMVSKx1h6lh2OenWaBTvMSK531hWaXAW7I0tKZepYug=
@ -56,6 +60,8 @@ github.com/diamondburned/cchat v0.0.46 h1:fzm2XA9uGasX0uaic1AFfUMGA53PlO+GGmkYbx
github.com/diamondburned/cchat v0.0.46/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.48 h1:MAzGzKY20JBh/LnirOZVPwbMq07xfqu4Lb4XsV9/sXQ=
github.com/diamondburned/cchat v0.0.48/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.49 h1:zP6QvjdRU3UqDZt3rEqjkR/5M68XRVms7htHfE9tLOc=
github.com/diamondburned/cchat v0.0.49/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401 h1:llmx/8UiJoTcHUw+GE5/rESVVmmnLh1HEPx3wRj+oQY=
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401/go.mod h1:+hSrIVYj5tIPLAorDsHj2Tbt2fWlZtOanzfEUHX53HM=
github.com/diamondburned/cchat-discord v0.0.0-20200730000036-2c93cdc1974e h1:EA5Vg0x57qLURJP80XhABBW+X0sbQSh2gw5qvPbZTs4=
@ -64,6 +70,16 @@ github.com/diamondburned/cchat-discord v0.0.0-20200815223744-cdd9b6804361 h1:vx3
github.com/diamondburned/cchat-discord v0.0.0-20200815223744-cdd9b6804361/go.mod h1:sW8tIqRcKux9bhMCtcqYI1fCMGCB23FoW67gcpr13fk=
github.com/diamondburned/cchat-discord v0.0.0-20200816234747-9647ad0709f0 h1:IVL4KUyLG9i5xPGwhIeKYttWHogenKiQLgfXEnYWNvU=
github.com/diamondburned/cchat-discord v0.0.0-20200816234747-9647ad0709f0/go.mod h1:Hl5AJstOnuP8rCdrL/Ns9W5MH09PRxQM4tG0BIMCjLw=
github.com/diamondburned/cchat-discord v0.0.0-20200818185457-528f40bb7258 h1:U7eOUozoKBW2ZBMffmWOVajvDi5GQhV3jlSgAW2jCHg=
github.com/diamondburned/cchat-discord v0.0.0-20200818185457-528f40bb7258/go.mod h1:EAsQW54v5lb74rjrTiBPwyF1WtR91k0tmaiCIuZs0po=
github.com/diamondburned/cchat-discord v0.0.0-20200819232051-b9d06fade536 h1:A6B9LX13NweRTd8A4lJqdsArH+RicnwA7BK59fZ6Kb4=
github.com/diamondburned/cchat-discord v0.0.0-20200819232051-b9d06fade536/go.mod h1:ssgZcN6paA8TQkdh91zdZVkOV9kT9PvGFskHLvg6zXw=
github.com/diamondburned/cchat-discord v0.0.0-20200820014012-0b17709222a0 h1:PH3oMGoe25eFyUyuGimeGk9e5IsN80k2ZYx+NA9HfAo=
github.com/diamondburned/cchat-discord v0.0.0-20200820014012-0b17709222a0/go.mod h1:ssgZcN6paA8TQkdh91zdZVkOV9kT9PvGFskHLvg6zXw=
github.com/diamondburned/cchat-discord v0.0.0-20200820222309-c85ad033d174 h1:EgEqbveuCKDBo6J7zaa4TT/dgYs8POojeFbHT1TSXTc=
github.com/diamondburned/cchat-discord v0.0.0-20200820222309-c85ad033d174/go.mod h1:gz+UVcC3VON4j8DjpLwH3xKQyqiDT4fyap8V5Rn+ACg=
github.com/diamondburned/cchat-discord v0.0.0-20200820222718-68cfafc4c318 h1:mRGZ2/Cjgw8O4l2ZOfdYhNUbO9VGBQCfLpxO5QCUoyo=
github.com/diamondburned/cchat-discord v0.0.0-20200820222718-68cfafc4c318/go.mod h1:rhUseXyWXTVw0Da8edbQMHU9I4LRQ2zcRB3zRqg/oe4=
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b h1:sq0MXjJc3yAOZvuolRxOpKQNvpMLyTmsECxQqdYgF5E=
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b/go.mod h1:+bAf0m2o5qH54DmYJ/lR1HeITV53ol0JaoKyFFx3m3E=
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI=
@ -84,6 +100,12 @@ github.com/diamondburned/ningen v0.1.1-0.20200816040956-857988325ce0 h1:c3G8NjcS
github.com/diamondburned/ningen v0.1.1-0.20200816040956-857988325ce0/go.mod h1:PIsJWdDhjgN9OiR+qrDPD8KGQ8UyFuRVrgs3Ewu6a3c=
github.com/diamondburned/ningen v0.1.1-0.20200816192443-0b6a02d498d2 h1:PodX8lfv7ZffUHUsaQUdIjZ50ONY8uCCXXUL7yzoSMQ=
github.com/diamondburned/ningen v0.1.1-0.20200816192443-0b6a02d498d2/go.mod h1:PIsJWdDhjgN9OiR+qrDPD8KGQ8UyFuRVrgs3Ewu6a3c=
github.com/diamondburned/ningen v0.1.1-0.20200818185419-0d3e89f25ee1 h1:ahr5xvEzrBIpRc3mrdihCd0iCUEawPixulbJk6MVAao=
github.com/diamondburned/ningen v0.1.1-0.20200818185419-0d3e89f25ee1/go.mod h1:vglus/9ENunGHSAZxzilq1ApNQMDtYBSG8V4r/vDY1o=
github.com/diamondburned/ningen v0.1.1-0.20200820222120-e86664f3f084 h1:nZiBsM/9Dw9Uxw8iBu3GO5m+BlYmqa3kVIP6LcKJPKQ=
github.com/diamondburned/ningen v0.1.1-0.20200820222120-e86664f3f084/go.mod h1:aXm69MB+Qu04OjBiixQw28zRijDc49vruMJMaHZ2c0Q=
github.com/diamondburned/ningen v0.1.1-0.20200820222640-35796f938a58 h1:sQvq5DgmX8hSyiP4HVZfOhlFY/PgvQWu6USAy04i1Yc=
github.com/diamondburned/ningen v0.1.1-0.20200820222640-35796f938a58/go.mod h1:aXm69MB+Qu04OjBiixQw28zRijDc49vruMJMaHZ2c0Q=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=

View File

@ -7,10 +7,11 @@ import (
"github.com/diamondburned/cchat-gtk/icons"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/actions"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
type header struct {
@ -51,7 +52,8 @@ func newHeader() *header {
}
}
const BreadcrumbSlash = `<span rise="-1024" size="x-large">/</span>`
// const BreadcrumbSlash = `<span rise="-1024" size="x-large">❭</span>`
const BreadcrumbSlash = " 〉"
func (h *header) SetBreadcrumber(b traverse.Breadcrumber) {
if b == nil {
@ -60,6 +62,13 @@ func (h *header) SetBreadcrumber(b traverse.Breadcrumber) {
}
var crumb = b.Breadcrumb()
if len(crumb) > 0 {
h.left.svcname.SetText(crumb[0])
} else {
h.left.svcname.SetText("")
}
for i := range crumb {
crumb[i] = html.EscapeString(crumb[i])
}
@ -104,6 +113,7 @@ func (a *appMenu) SetSizeRequest(w, h int) {
type headerLeft struct {
*gtk.Box
appmenu *appMenu
svcname *gtk.Label
sesmenu *actions.MenuButton
}
@ -115,17 +125,25 @@ func newHeaderLeft() *headerLeft {
sep.Show()
primitives.AddClass(sep, "titlebutton")
svcname, _ := gtk.LabelNew("")
svcname.SetXAlign(0)
svcname.SetEllipsize(pango.ELLIPSIZE_END)
svcname.Show()
svcname.SetMarginStart(14)
sesmenu := actions.NewMenuButton()
sesmenu.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(appmenu, false, false, 0)
box.PackStart(sep, false, false, 0)
box.PackStart(svcname, true, true, 0)
box.PackStart(sesmenu, false, false, 5)
return &headerLeft{
Box: box,
appmenu: appmenu,
svcname: svcname,
sesmenu: sesmenu,
}
}

View File

@ -15,6 +15,8 @@ const BacklogLimit = 35
type GridMessage interface {
message.Container
// Focusable should return a widget that can be focused.
Focusable() gtk.IWidget
// Attach should only be called once.
Attach(grid *gtk.Grid, row int)
// AttachMenu should override the stored constructor.
@ -39,18 +41,28 @@ type Container interface {
// Thread-safe methods.
cchat.MessagesContainer
cchat.MessagePrepender
// Thread-unsafe methods.
CreateMessageUnsafe(cchat.MessageCreate)
UpdateMessageUnsafe(cchat.MessageUpdate)
DeleteMessageUnsafe(cchat.MessageDelete)
PrependMessageUnsafe(cchat.MessageCreate)
Reset()
// FirstMessage returns the first message in the buffer. Nil is returned if
// there's nothing.
FirstMessage() GridMessage
// TranslateCoordinates is used for scrolling to the message.
TranslateCoordinates(parent gtk.IWidget, msg GridMessage) (y int)
// AddPresendMessage adds and displays an unsent message.
AddPresendMessage(msg input.PresendMessage) PresendGridMessage
// LatestMessageFrom returns the last message ID with that author.
LatestMessageFrom(authorID string) (msgID string, ok bool)
// UI methods.
SetFocusHAdjustment(*gtk.Adjustment)
SetFocusVAdjustment(*gtk.Adjustment)
}
// Controller is for menu actions.
@ -132,3 +144,7 @@ func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
gts.ExecAsync(func() { c.DeleteMessageUnsafe(msg) })
}
func (c *GridContainer) PrependMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() { c.PrependMessageUnsafe(msg) })
}

View File

@ -57,11 +57,14 @@ func NewContainer(ctrl container.Controller) *Container {
}
func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
// Is the latest message of the same author? If yes, display it as a
// collapsed message.
if c.lastMessageIsAuthor(msg.Author().ID()) {
return NewCollapsedMessage(msg)
}
// We're not checking for a collapsed message here anymore, as the
// CreateMessage method will do that.
// // Is the latest message of the same author? If yes, display it as a
// // collapsed message.
// if c.lastMessageIsAuthor(msg.Author().ID()) {
// return NewCollapsedMessage(msg)
// }
full := NewFullMessage(msg)
author := msg.Author()
@ -77,7 +80,9 @@ func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
}
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
if c.lastMessageIsAuthor(msg.AuthorID()) {
// We can do the check here since we're never using NewPresendMessage for
// backlog messages.
if c.lastMessageIsAuthor(msg.AuthorID(), 0) {
return NewCollapsedSendingMessage(msg)
}
@ -90,11 +95,6 @@ func (c *Container) NewPresendMessage(msg input.PresendMessage) container.Presen
return full
}
func (c *Container) lastMessageIsAuthor(id string) bool {
var last = c.GridStore.LastMessage()
return last != nil && last.AuthorID() == id
}
func (c *Container) findAuthorID(authorID string) container.GridMessage {
// Search the old author if we have any.
return c.GridStore.FindMessage(func(msgc container.GridMessage) bool {
@ -120,12 +120,23 @@ func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) {
}
}
func (c *Container) lastMessageIsAuthor(id string, offset int) bool {
var last = c.GridStore.NthMessage(c.GridStore.MessagesLen() - (1 + offset))
return last != nil && last.AuthorID() == id
}
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
// Create the message in the parent's handler. This handler will also
// wipe old messages.
c.GridContainer.CreateMessageUnsafe(msg)
// Should we collapse this message? Yes, if the current message's author
// is the same as the last author.
if c.lastMessageIsAuthor(msg.Author().ID(), 1) {
c.compact(c.GridContainer.LastMessage())
}
// Did the handler wipe old messages? It will only do so if the user is
// scrolled to the bottom.
if !c.Bottomed() {
@ -199,3 +210,37 @@ func (c *Container) uncompact(msg container.GridMessage) {
// Swap the old next message out for a new one.
c.GridStore.SwapMessage(full)
}
func (c *Container) PrependMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
c.GridContainer.PrependMessageUnsafe(msg)
// See if we need to uncollapse the second message.
if sec := c.NthMessage(1); sec != nil {
// If the author isn't the same, then ignore.
if sec.AuthorID() != msg.Author().ID() {
return
}
// The author is the same; collapse.
c.compact(sec)
}
})
}
func (c *Container) compact(msg container.GridMessage) {
// Exit if the message is already collapsed.
if collapse, ok := msg.(Collapsible); !ok || collapse.Collapsed() {
return
}
uw, ok := msg.(Unwrapper)
if !ok {
return
}
compact := WrapCollapsedMessage(uw.Unwrap(c.Grid))
message.RefreshContainer(compact, compact.GenericContainer)
c.GridStore.SwapMessage(compact)
}

View File

@ -59,6 +59,10 @@ func (c *CollapsedMessage) Attach(grid *gtk.Grid, row int) {
container.AttachRow(grid, row, c.Timestamp, c.Content)
}
func (c *CollapsedMessage) Focusable() gtk.IWidget {
return c.Timestamp
}
type CollapsedSendingMessage struct {
*CollapsedMessage
message.PresendContainer

View File

@ -114,10 +114,26 @@ func (m *FullMessage) Unwrap(grid *gtk.Grid) *message.GenericContainer {
m.HeaderBox.Remove(m.Timestamp)
m.MainBox.Remove(m.Content)
// Hide the avatar.
m.Avatar.Hide()
// Remove the message from the grid.
grid.Remove(m.Avatar)
grid.Remove(m.MainBox)
// Return after removing.
return m.GenericContainer
}
func (m *FullMessage) Attach(grid *gtk.Grid, row int) {
m.Avatar.Show()
container.AttachRow(grid, row, m.Avatar, m.MainBox)
}
func (m *FullMessage) Focusable() gtk.IWidget {
return m.Avatar
}
func (m *FullMessage) UpdateTimestamp(t time.Time) {
m.GenericContainer.UpdateTimestamp(t)
m.Timestamp.SetText(humanize.TimeAgoLong(t))
@ -136,7 +152,7 @@ func (m *FullMessage) UpdateAuthor(author cchat.MessageAuthor) {
// CopyAvatarPixbuf sets the pixbuf into the given container. This shares the
// same pixbuf, but gtk.Image should take its own reference from the pixbuf.
func (m *FullMessage) CopyAvatarPixbuf(dst httputil.ImageContainer) {
switch img := m.Avatar.Image; img.GetStorageType() {
switch img := m.Avatar.Image; img.GetImage().GetStorageType() {
case gtk.IMAGE_PIXBUF:
dst.SetFromPixbuf(img.GetPixbuf())
case gtk.IMAGE_ANIMATION:
@ -144,11 +160,6 @@ func (m *FullMessage) CopyAvatarPixbuf(dst httputil.ImageContainer) {
}
}
func (m *FullMessage) Attach(grid *gtk.Grid, row int) {
m.Avatar.Show()
container.AttachRow(grid, row, m.Avatar, m.MainBox)
}
func (m *FullMessage) AttachMenu(items []menu.Item) {
// Bind to parent's container as well.
m.GenericContainer.AttachMenu(items)
@ -183,17 +194,12 @@ type Avatar struct {
}
func NewAvatar() *Avatar {
avatar, _ := roundimage.NewEmptyButton()
avatar.SetSizeRequest(AvatarSize, AvatarSize)
avatar.SetVAlign(gtk.ALIGN_START)
img, _ := roundimage.NewStaticImage(avatar, 0)
img, _ := roundimage.NewStaticImage(nil, 0)
img.Show()
avatar.SetImage(img.Image)
// TODO
// Remove static image; make it internal; make static iamge bind to something else incl button and list
avatar, _ := roundimage.NewCustomButton(img)
avatar.SetSizeRequest(AvatarSize, AvatarSize)
avatar.SetVAlign(gtk.ALIGN_START)
// Default icon.
primitives.SetImageIcon(img, "user-available-symbolic", AvatarSize)

View File

@ -8,6 +8,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
type GridStore struct {
@ -38,18 +39,6 @@ func NewGridStore(constr Constructor, ctrl Controller) *GridStore {
}
}
func (c *GridStore) Reset() {
c.Grid.GetChildren().Foreach(func(v interface{}) {
// Unsafe assertion ftw.
w := v.(gtk.IWidget).ToWidget()
c.Grid.Remove(w)
w.Destroy()
})
c.messages = map[string]*gridMessage{}
c.messageIDs = []string{}
}
func (c *GridStore) MessagesLen() int {
return len(c.messages)
}
@ -64,6 +53,34 @@ func (c *GridStore) findIndex(idnonce string) int {
return -1
}
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) {
i := c.findIndex(msg.ID())
if i < 0 {
return 0
}
m, _ := c.messages[c.messageIDs[i]]
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
}
// log.Println("X:", x)
// log.Println("Y:", y)
return y
}
// 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.
//
@ -75,12 +92,21 @@ func (c *GridStore) SwapMessage(msg GridMessage) bool {
return false
}
// Wrap msg inside a *gridMessage if it's not already.
mg, ok := msg.(*gridMessage)
if !ok {
mg = &gridMessage{GridMessage: msg}
}
// Add a row at index. The actual row we want to delete will be shifted
// downwards.
c.Grid.InsertRow(ix)
// Let the new message be attached on top of the to-be-replaced message.
msg.Attach(c.Grid, ix)
mg.Attach(c.Grid, ix)
// Set the message into the map.
c.messages[mg.ID()] = mg
// Delete the to-be-replaced message, which we have shifted downwards
// earlier, so we add 1.
@ -146,20 +172,22 @@ func (c *GridStore) FindMessage(isMessage func(msg GridMessage) bool) GridMessag
return nil
}
// FirstMessage returns the first message.
func (c *GridStore) FirstMessage() GridMessage {
if len(c.messageIDs) > 0 {
return c.messages[c.messageIDs[0]].GridMessage
// NthMessage returns the nth message.
func (c *GridStore) NthMessage(n int) GridMessage {
if len(c.messageIDs) > 0 && n >= 0 && n < len(c.messageIDs) {
return c.messages[c.messageIDs[n]].GridMessage
}
return nil
}
// FirstMessage returns the first message.
func (c *GridStore) FirstMessage() GridMessage {
return c.NthMessage(0)
}
// LastMessage returns the latest message.
func (c *GridStore) LastMessage() GridMessage {
if l := len(c.messageIDs); l > 0 {
return c.messages[c.messageIDs[l-1]].GridMessage
}
return nil
return c.NthMessage(c.MessagesLen() - 1)
}
// Message finds the message state in the container. It is not thread-safe. This
@ -228,6 +256,25 @@ func (c *GridStore) AddPresendMessage(msg input.PresendMessage) PresendGridMessa
return presend
}
func (c *GridStore) PrependMessageUnsafe(msg cchat.MessageCreate) {
msgc := &gridMessage{
GridMessage: c.Construct.NewMessage(msg),
}
c.Grid.InsertRow(0)
msgc.Attach(c.Grid, 0)
// Prepend the message ID.
c.messageIDs = append(c.messageIDs, "")
copy(c.messageIDs[1:], c.messageIDs)
c.messageIDs[0] = msgc.ID()
// Set the message into the map.
c.messages[msgc.ID()] = msgc
c.Controller.BindMenu(msgc)
}
func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())

View File

@ -22,8 +22,9 @@ import (
var MemberListWidth = 250
type Container struct {
*gtk.ScrolledWindow
Main *gtk.Box
*gtk.Revealer
Scroll *gtk.ScrolledWindow
Main *gtk.Box
// map id -> *Section
Sections map[string]*Section
@ -47,11 +48,19 @@ func New() *Container {
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
sw.Add(main)
sw.Show()
rev, _ := gtk.RevealerNew()
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
rev.SetTransitionDuration(50)
rev.SetRevealChild(false)
rev.Add(sw)
return &Container{
ScrolledWindow: sw,
Main: main,
Sections: map[string]*Section{},
Revealer: rev,
Scroll: sw,
Main: main,
Sections: map[string]*Section{},
}
}
@ -62,6 +71,8 @@ func (c *Container) Reset() {
c.stop = nil
}
c.Revealer.SetRevealChild(false)
for _, section := range c.Sections {
c.Main.Remove(section)
}
@ -85,6 +96,7 @@ func (c *Container) TryAsyncList(server cchat.ServerMessage) {
return func() {
c.stop = f
c.Revealer.SetRevealChild(true)
}, nil
})
}
@ -363,7 +375,9 @@ func (m *Member) Update(member cchat.ListMember) {
func (m *Member) Popup() {
if len(m.output.Mentions) > 0 {
p := labeluri.NewPopoverMentioner(m, m.output.Input, m.output.Mentions[0])
p.Ref() // prevent the popover from closing itself
p.SetPosition(gtk.POS_LEFT)
p.Connect("closed", p.Unref)
p.Popup()
}
}

View File

@ -210,3 +210,7 @@ func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
func (m *GenericContainer) AttachMenu(newItems []menu.Item) {
m.MenuItems = newItems
}
func (m *GenericContainer) Focusable() gtk.IWidget {
return m.Content
}

View File

@ -55,6 +55,14 @@ func (v *FaceView) Reset() {
v.Stack.SetVisibleChildName("empty")
}
// func (v *FaceView) Disable() {
// v.Stack.SetSensitive(false)
// }
// func (v *FaceView) Enable() {
// v.Stack.SetSensitive(true)
// }
func (v *FaceView) SetMain() {
v.ensurePlaceholderDestroyed()
v.Loading.Spinner.Stop()

View File

@ -2,6 +2,7 @@ package messages
import (
"context"
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/icons"
@ -38,6 +39,15 @@ func init() {
))
}
type Controller interface {
// OnMessageBusy is called when the message buffer is busy. This happens
// when it's loading messages.
OnMessageBusy()
// OnMessageDone is called after OnMessageBusy, when the message buffer is
// done with loading.
OnMessageDone()
}
type View struct {
*sadface.FaceView
Grid *gtk.Grid
@ -54,10 +64,12 @@ type View struct {
// Inherit some useful methods.
state
ctrl Controller
}
func NewView() *View {
view := &View{}
func NewView(c Controller) *View {
view := &View{ctrl: c}
view.Typing = typing.New()
view.Typing.Show()
@ -68,16 +80,26 @@ func NewView() *View {
view.MsgBox.PackEnd(view.Typing, false, false, 0)
view.MsgBox.Show()
// Create the message container, which will use PackEnd to add the widget on
// TOP of the typing indicator.
view.createMessageContainer()
view.Scroller = autoscroll.NewScrolledWindow()
view.Scroller.Add(view.MsgBox)
view.Scroller.SetVExpand(true)
view.Scroller.SetHExpand(true)
view.Scroller.Show()
view.MsgBox.SetFocusHAdjustment(view.Scroller.GetHAdjustment())
view.MsgBox.SetFocusVAdjustment(view.Scroller.GetVAdjustment())
// Create the message container, which will use PackEnd to add the widget on
// TOP of the typing indicator.
view.createMessageContainer()
// Fetch the message backlog when the user has scrolled to the top.
view.Scroller.Connect("edge-reached", func(_ *gtk.ScrolledWindow, p gtk.PositionType) {
if p == gtk.POS_TOP {
view.FetchBacklog()
}
})
// A separator to go inbetween.
sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
sep.SetHExpand(true)
@ -121,6 +143,9 @@ func (v *View) createMessageContainer() {
v.Container = compact.NewContainer(v)
}
v.Container.SetFocusHAdjustment(v.Scroller.GetHAdjustment())
v.Container.SetFocusVAdjustment(v.Scroller.GetVAdjustment())
// Add the new message container.
v.MsgBox.PackEnd(v.Container, true, true, 0)
}
@ -132,25 +157,23 @@ func (v *View) Reset() {
v.Typing.Reset() // Reset the typing state.
v.InputView.Reset() // Reset the input.
v.MemberList.Reset() // Reset the member list.
v.Container.Reset() // Clean all messages.
v.FaceView.Reset() // Switch back to the main screen.
// Keep the scroller at the bottom.
v.Scroller.Bottomed = true
// Recreate the message container if the type is different.
if v.contType != msgIndex {
v.createMessageContainer()
}
// Reallocate the entire message container.
v.createMessageContainer()
}
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func()) {
func (v *View) JoinServer(session cchat.Session, server ServerMessage) {
// Reset before setting.
v.Reset()
// Set the screen to loading.
v.FaceView.SetLoading()
v.ctrl.OnMessageBusy()
// Bind the state.
v.state.bind(session, server)
@ -171,12 +194,12 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func
err = errors.Wrap(err, "Failed to join server")
// Even if we're erroring out, we're running the done() callback
// anyway.
return func() { done(); v.SetError(err) }, err
return func() { v.ctrl.OnMessageDone(); v.SetError(err) }, err
}
return func() {
// Run the done() callback.
done()
v.ctrl.OnMessageDone()
// Set the screen to the main one.
v.FaceView.SetMain()
@ -193,6 +216,34 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func
})
}
func (v *View) FetchBacklog() {
var backlogger = v.state.Backlogger()
if backlogger == nil {
return
}
var firstMsg = v.Container.FirstMessage()
if firstMsg == nil {
return
}
// Set the window as busy. TODO: loading circles.
v.ctrl.OnMessageBusy()
var done = func() {
v.ctrl.OnMessageDone()
// Restore scrolling.
y := v.Container.TranslateCoordinates(v.MsgBox, firstMsg)
v.Scroller.GetVAdjustment().SetValue(float64(y))
}
gts.Async(func() (func(), error) {
err := backlogger.MessagesBefore(context.Background(), firstMsg.ID(), v.Container)
return done, errors.Wrap(err, "Failed to get messages before ID")
})
}
func (v *View) AddPresendMessage(msg input.PresendMessage) func(error) {
var presend = v.Container.AddPresendMessage(msg)
@ -290,10 +341,13 @@ type state struct {
session cchat.Session
server cchat.Server
actioner cchat.ServerMessageActioner
actioner cchat.ServerMessageActioner
backlogger cchat.ServerMessageBacklogger
current func() // stop callback
author string
lastBacklogged time.Time
}
func (s *state) Reset() {
@ -318,10 +372,30 @@ func (s *state) SessionID() string {
return ""
}
const backloggingFreq = time.Second * 3
// Backlogger returns the backlogger instance if it's allowed to fetch more
// backlogs.
func (s *state) Backlogger() cchat.ServerMessageBacklogger {
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 ServerMessage) {
s.session = session
s.server = server
s.actioner, _ = server.(cchat.ServerMessageActioner)
s.backlogger, _ = server.(cchat.ServerMessageBacklogger)
}
func (s *state) setcurrent(fn func()) {

View File

@ -11,6 +11,7 @@ type ScrolledWindow struct {
func NewScrolledWindow() *ScrolledWindow {
gtksw, _ := gtk.ScrolledWindowNew(nil, nil)
gtksw.SetProperty("propagate-natural-height", true)
gtksw.SetProperty("window-placement", gtk.CORNER_BOTTOM_LEFT)
sw := &ScrolledWindow{*gtksw, *gtksw.GetVAdjustment(), true} // bottomed by default
sw.Connect("size-allocate", func(_ *gtk.ScrolledWindow) {

View File

@ -19,7 +19,7 @@ const (
// supports a full circle for rounding.
type Button struct {
*gtk.Button
Image *Image
Image Imager
}
var roundButtonCSS = primitives.PrepareClassCSS("round-button", `
@ -47,7 +47,21 @@ func NewEmptyButton() (*Button, error) {
return &Button{Button: b}, nil
}
func (b *Button) SetImage(img *Image) {
// NewCustomButton creates a new rounded button with the given Imager. If the
// given Imager implements the Connector interface (aka *StaticImage), then the
// function will implicitly connect its handlers to the button.
func NewCustomButton(img Imager) (*Button, error) {
b, _ := NewEmptyButton()
b.SetImage(img)
if connector, ok := img.(Connector); ok {
connector.ConnectHandlers(b)
}
return b, nil
}
func (b *Button) SetImage(img Imager) {
b.Image = img
b.Button.SetImage(img)
}
@ -56,13 +70,34 @@ type RadiusSetter interface {
SetRadius(float64)
}
type Connector interface {
ConnectHandlers(connector primitives.Connector)
}
type Imager interface {
gtk.IWidget
RadiusSetter
// Embed setters.
httputil.ImageContainerSizer
GetPixbuf() *gdk.Pixbuf
GetAnimation() *gdk.PixbufAnimation
GetImage() *gtk.Image
}
// StaticImage is an image that only plays a GIF if it's hovered on top of.
type StaticImage struct {
*Image
animation *gdk.PixbufAnimation
}
var _ httputil.ImageContainer = (*StaticImage)(nil)
var (
_ Imager = (*StaticImage)(nil)
_ Connector = (*StaticImage)(nil)
_ httputil.ImageContainer = (*StaticImage)(nil)
)
func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage, error) {
i, err := NewImage(radius)
@ -110,6 +145,8 @@ type Image struct {
Radius float64
}
var _ Imager = (*Image)(nil)
// NewImage creates a new round image. If radius is 0, then it will be half the
// dimensions. If the radius is less than 0, then nothing is rounded.
func NewImage(radius float64) (*Image, error) {
@ -126,6 +163,10 @@ func NewImage(radius float64) (*Image, error) {
return image, nil
}
func (i *Image) GetImage() *gtk.Image {
return i.Image
}
func (i *Image) SetRadius(r float64) {
i.Radius = r
}

View File

@ -187,24 +187,27 @@ func largeText(text string) string {
// popoverImg creates a new button with an image for it, which is used for the
// avatar in the user popover.
func popoverImg(url string, round bool) gtk.IWidget {
var img *gtk.Image
var btn *gtk.Button
var img *gtk.Image
var idl httputil.ImageContainerSizer
if round {
b, _ := roundimage.NewButton()
img = b.Image.Image
img = b.Image.GetImage()
idl = b.Image
btn = b.Button
} else {
img, _ = gtk.ImageNew()
btn, _ = gtk.ButtonNew()
btn.Add(img)
idl = img
}
img.SetSizeRequest(AvatarSize, AvatarSize)
img.SetHAlign(gtk.ALIGN_CENTER)
img.Show()
httputil.AsyncImageSized(img, url, AvatarSize, AvatarSize)
httputil.AsyncImageSized(idl, url, AvatarSize, AvatarSize)
btn.SetHAlign(gtk.ALIGN_CENTER)
btn.SetRelief(gtk.RELIEF_NONE)

View File

@ -89,6 +89,9 @@ func (c *Children) Reset() {
if c.Box != nil {
// Remove old servers from the list.
for _, row := range c.Rows {
if row.IsHollow() {
continue
}
c.Box.Remove(row)
}
}

View File

@ -316,9 +316,11 @@ func (r *Row) SetSelected(selected bool) {
}
func (r *Row) GetActive() bool {
AssertUnhollow(r)
if !r.IsHollow() {
return r.Button.GetActive()
}
return r.Button.GetActive()
return false
}
// SetRevealChild reveals the list of servers. It does nothing if there are no

View File

@ -133,15 +133,19 @@ func (app *App) RowSelected(ses *session.Row, srv *server.ServerRow, smsg cchat.
app.header.SetBreadcrumber(srv)
// Assert that server is also a list, then join the server.
app.window.MessageView.JoinServer(ses.Session, smsg.(messages.ServerMessage))
}
func (app *App) OnMessageBusy() {
// Disable the server list because we don't want the user to switch around
// while we're loading.
app.window.Services.SetSensitive(false)
gts.App.Window.SetSensitive(false)
}
// Assert that server is also a list, then join the server.
app.window.MessageView.JoinServer(ses.Session, smsg.(messages.ServerMessage), func() {
// Re-enable the server list.
app.window.Services.SetSensitive(true)
})
func (app *App) OnMessageDone() {
// Re-enable the server list.
gts.App.Window.SetSensitive(true)
}
func (app *App) AuthenticateSession(list *service.List, ssvc *service.Service) {

View File

@ -12,12 +12,17 @@ type window struct {
MessageView *messages.View
}
func newWindow(mainctl service.Controller) *window {
type Controller interface {
service.Controller
messages.Controller
}
func newWindow(mainctl Controller) *window {
services := service.NewView(mainctl)
services.SetSizeRequest(leftMinWidth, -1)
services.Show()
mesgview := messages.NewView()
mesgview := messages.NewView(mainctl)
mesgview.Show()
pane, _ := gtk.PanedNew(gtk.ORIENTATION_HORIZONTAL)

View File

@ -3,23 +3,29 @@
package main
import (
"os"
"runtime/pprof"
_ "net/http/pprof"
_ "github.com/ianlancetaylor/cgosymbolizer"
)
import "C"
const ProfileAddr = "localhost:49583"
func init() {
C.HeapProfilerStart()
destructor = func() { C.HeapProfilerStop() }
// runtime.SetBlockProfileRate(1)
// go func() {
// if err := http.ListenAndServe("localhost:42069", nil); err != nil {
// log.Println("Listening to profiler at", ProfileAddr)
// if err := http.ListenAndServe(ProfileAddr, nil); err != nil {
// log.Error(errors.Wrap(err, "Failed to start profiling HTTP server"))
// }
// }()
// runtime.SetBlockProfileRate(1)
// f, _ := os.Create("/tmp/cchat.pprof")
// p := pprof.Lookup("block")
@ -33,13 +39,13 @@ func init() {
// f.Close()
// }
f, _ := os.Create("/tmp/cchat.pprof")
if err := pprof.StartCPUProfile(f); err != nil {
panic(err)
}
// f, _ := os.Create("/tmp/cchat.pprof")
// if err := pprof.StartCPUProfile(f); err != nil {
// panic(err)
// }
destructor = func() {
pprof.StopCPUProfile()
f.Close()
}
// destructor = func() {
// pprof.StopCPUProfile()
// f.Close()
// }
}