Refactor server list, minor appearance tweaks

This commit is contained in:
diamondburned 2021-05-01 00:26:17 -07:00
parent e73f9a099b
commit 095107180d
24 changed files with 438 additions and 344 deletions

4
go.mod
View File

@ -2,13 +2,11 @@ module github.com/diamondburned/cchat-gtk
go 1.16 go 1.16
replace github.com/diamondburned/cchat-discord => ../cchat-discord
require ( require (
github.com/Xuanwo/go-locale v1.0.0 github.com/Xuanwo/go-locale v1.0.0
github.com/alecthomas/chroma v0.7.3 github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.6.4 github.com/diamondburned/cchat v0.6.4
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799
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-20210329054445-387ad28eb2c2 github.com/diamondburned/handy v0.0.0-20210329054445-387ad28eb2c2
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972

2
go.sum
View File

@ -145,6 +145,8 @@ github.com/diamondburned/cchat-discord v0.0.0-20210326063215-9eb392a95413 h1:r6P
github.com/diamondburned/cchat-discord v0.0.0-20210326063215-9eb392a95413/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs= github.com/diamondburned/cchat-discord v0.0.0-20210326063215-9eb392a95413/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff h1:p5XYPavnJ89wrJAf4ns6f1OfHQz5NMU9uXlX3EiKdfU= github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff h1:p5XYPavnJ89wrJAf4ns6f1OfHQz5NMU9uXlX3EiKdfU=
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs= github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799 h1:xxqeuAx0T9SsS8DYKe4jxzL2saEpLyQeAttD0sX/g1E=
github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
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=

View File

@ -96,8 +96,6 @@ func Restore() {
log.Error(errors.Wrap(err, "Failed to unmarshal main config.json")) log.Error(errors.Wrap(err, "Failed to unmarshal main config.json"))
} }
log.Printlnf("To restore: %#v", toRestore)
for path, v := range toRestore { for path, v := range toRestore {
if err := UnmarshalFromFile(path, v); err != nil { if err := UnmarshalFromFile(path, v); err != nil {
log.Error(errors.Wrapf(err, "Failed to unmarshal %s", path)) log.Error(errors.Wrapf(err, "Failed to unmarshal %s", path))

View File

@ -22,17 +22,23 @@ func NewContainer(ctrl container.Controller) *Container {
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow { func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
msg := WrapPresendMessage(state) msg := WrapPresendMessage(state)
c.AddMessage(msg) c.addMessage(msg)
return msg return msg
} }
func (c *Container) CreateMessage(msg cchat.MessageCreate) { func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() { gts.ExecAsync(func() {
msg := WrapMessage(message.NewState(msg)) msg := WrapMessage(message.NewState(msg))
c.ListContainer.AddMessage(msg) c.addMessage(msg)
c.CleanMessages()
}) })
} }
func (c *Container) addMessage(msg container.MessageRow) {
_, at := container.InsertPosition(c, msg.Unwrap().Time)
c.AddMessageAt(msg, at)
}
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) { func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
gts.ExecAsync(func() { container.UpdateMessage(c, msg) }) gts.ExecAsync(func() { container.UpdateMessage(c, msg) })
} }

View File

@ -94,13 +94,9 @@ func (m Message) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
m.Username.SetReferenceHighlighter(r) m.Username.SetReferenceHighlighter(r)
} }
func (m Message) Unwrap(revert bool) *message.State { func (m Message) Revert() *message.State {
if revert { m.unwrap()
m.unwrap() m.ClearBox()
primitives.RemoveChildren(m) return m.Unwrap()
m.SetClass("")
}
return m.State
} }

View File

@ -1,6 +1,8 @@
package container package container
import ( import (
"time"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"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"
@ -37,21 +39,21 @@ type Container interface {
// NewPresendMessage creates and adds a presend message state into the list. // NewPresendMessage creates and adds a presend message state into the list.
NewPresendMessage(state *message.PresendState) PresendMessageRow NewPresendMessage(state *message.PresendState) PresendMessageRow
// AddMessage adds a new message into the list. // AddMessageAt adds a new message into the list at the given index.
AddMessage(row MessageRow) AddMessageAt(row MessageRow, ix int)
// FirstMessage returns the first message in the buffer. Nil is returned if // MessagesLen returns the current number of messages.
// there's nothing. MessagesLen() int
FirstMessage() MessageRow // NthMessage returns the nth message in the buffer or nil if there's
// LastMessage returns the last message in the buffer or nil if there's
// nothing. // nothing.
LastMessage() MessageRow NthMessage(ix int) MessageRow
// Message finds and returns the message, if any. It performs maximum 2 // Message finds and returns the message, if any. It performs maximum 2
// constant-time lookups. // constant-time lookups.
Message(id cchat.ID, nonce string) MessageRow Message(id cchat.ID, nonce string) MessageRow
// FindMessage finds a message that satisfies the given callback. It // FindMessage finds a message that satisfies the given callback. It
// iterates the message buffer from latest to earliest. // iterates the message buffer from latest to earliest.
FindMessage(isMessage func(MessageRow) bool) MessageRow FindMessage(isMessage func(MessageRow) bool) (MessageRow, int)
// Highlight temporarily highlights the given message for a short while. // Highlight temporarily highlights the given message for a short while.
Highlight(msg MessageRow) Highlight(msg MessageRow)
@ -71,10 +73,56 @@ func UpdateMessage(ct Container, update cchat.MessageUpdate) {
} }
// LatestMessageFrom returns the latest message from the given author ID. // LatestMessageFrom returns the latest message from the given author ID.
func LatestMessageFrom(ct Container, authorID cchat.ID) MessageRow { func LatestMessageFrom(ct Container, authorID cchat.ID) (MessageRow, int) {
return ct.FindMessage(func(msg MessageRow) bool { finder, ok := ct.(messageFinder)
return msg.Unwrap(false).Author.ID == authorID if !ok {
return ct.FindMessage(func(msg MessageRow) bool {
return msg.Unwrap().Author.ID == authorID
})
}
msg, ix := finder.findMessage(true, func(msg *messageRow) bool {
return msg.state.Author.ID == authorID
}) })
return unwrapRow(msg), ix
}
// FirstMessage returns the first message in the buffer. Nil is returned if
// there's nothing.
func FirstMessage(ct Container) MessageRow {
return ct.NthMessage(0)
}
// LastMessage returns the last message in the buffer or nil if there's nothing.
func LastMessage(ct Container) MessageRow {
return ct.NthMessage(ct.MessagesLen() - 1)
}
// InsertPosition returns the message that is before the given time (or nil) and
// the new index of the message with the given timestamp. If -1 is returned,
// then there is no message prior, and the message should be prepended on top.
func InsertPosition(ct Container, t time.Time) (MessageRow, int) {
var row MessageRow
var mIx int
finder, ok := ct.(messageFinder)
if !ok {
row, mIx = ct.FindMessage(func(msg MessageRow) bool {
return t.After(msg.Unwrap().Time)
})
} else {
// Iterate and compare timestamp to find where to insert a message. Note
// that "before" is the message that will go before the to-be-inserted
// method.
msg, ix := finder.findMessage(true, func(msg *messageRow) bool {
return t.After(msg.state.Time)
})
row = unwrapRow(msg)
mIx = ix
}
return row, mIx
} }
// Controller is for menu actions. // Controller is for menu actions.
@ -109,6 +157,7 @@ type ListContainer struct {
// messageRow w/ required internals // messageRow w/ required internals
type messageRow struct { type messageRow struct {
MessageRow MessageRow
state *message.State
presend message.Presender // this shouldn't be here but i'm lazy presend message.Presender // this shouldn't be here but i'm lazy
} }
@ -140,11 +189,6 @@ func NewListContainer(ctrl Controller) *ListContainer {
} }
} }
func (c *ListContainer) AddMessage(row MessageRow) {
c.ListStore.AddMessage(row)
c.CleanMessages()
}
// CleanMessages cleans up the oldest messages if the user is scrolled to the // CleanMessages cleans up the oldest messages if the user is scrolled to the
// bottom. True is returned if there were changes. // bottom. True is returned if there were changes.
func (c *ListContainer) CleanMessages() bool { func (c *ListContainer) CleanMessages() bool {

View File

@ -14,7 +14,7 @@ type Avatar struct {
} }
func NewAvatar(parent primitives.Connector) *Avatar { func NewAvatar(parent primitives.Connector) *Avatar {
img := roundimage.NewStillImage(nil, 0) img := roundimage.NewStillImage(parent, 0)
img.SetSizeRequest(AvatarSize, AvatarSize) img.SetSizeRequest(AvatarSize, AvatarSize)
img.Show() img.Show()

View File

@ -10,20 +10,6 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
) )
// Collapsible is an interface for cozy messages to return whether or not
// they're full or collapsed.
type Collapsible interface {
// Compact returns true if the message is a compact one and not full.
Collapsed() bool
}
var (
_ Collapsible = (*CollapsedMessage)(nil)
_ Collapsible = (*CollapsedSendingMessage)(nil)
_ Collapsible = (*FullMessage)(nil)
_ Collapsible = (*FullSendingMessage)(nil)
)
const ( const (
AvatarSize = 40 AvatarSize = 40
AvatarMargin = 10 AvatarMargin = 10
@ -61,69 +47,58 @@ func NewContainer(ctrl container.Controller) *Container {
return &Container{ListContainer: c} return &Container{ListContainer: c}
} }
func (c *Container) findAuthorID(authorID string) container.MessageRow {
// Search the old author if we have any.
return c.ListStore.FindMessage(func(msgc container.MessageRow) bool {
return msgc.Unwrap(false).Author.ID == authorID
})
}
const splitDuration = 10 * time.Minute const splitDuration = 10 * time.Minute
// isCollapsible returns true if the given lastMsg has matching conditions with // isCollapsible returns true if the given lastMsg has matching conditions with
// the given msg. // the given msg.
func isCollapsible(last container.MessageRow, msg *message.State) bool { func isCollapsible(last container.MessageRow, msg *message.State) bool {
if last == nil || msg.ID == "" { if last == nil || msg == nil {
return false return false
} }
lastMsg := last.Unwrap(false) lastMsg := last.Unwrap()
return true && return true &&
lastMsg.Author.ID == msg.ID && lastMsg.Author.ID == msg.Author.ID &&
lastMsg.Time.Add(splitDuration).After(msg.Time) lastMsg.Time.Add(splitDuration).After(msg.Time)
} }
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow { func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
msgr := NewPresendMessage(state, c.LastMessage()) before, at := container.InsertPosition(c, state.Time)
c.AddMessage(msgr) msgr := NewPresendMessage(state, before)
c.AddMessageAt(msgr, at)
return msgr return msgr
} }
func (c *Container) CreateMessage(msg cchat.MessageCreate) { func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() { gts.ExecAsync(func() {
before, at := container.InsertPosition(c, msg.Time())
state := message.NewState(msg) state := message.NewState(msg)
msgr := NewMessage(state, c.LastMessage()) msgr := NewMessage(state, before)
c.AddMessageAt(msgr, at)
c.AddMessage(msgr)
}) })
} }
// AddMessage adds the given message. // AddMessage adds the given message.
func (c *Container) AddMessage(msgr container.MessageRow) { func (c *Container) AddMessageAt(msgr container.MessageRow, ix int) {
// Create the message in the parent's handler. This handler will also // Create the message in the parent's handler. This handler will also
// wipe old messages. // wipe old messages.
c.ListContainer.AddMessage(msgr) c.ListContainer.AddMessageAt(msgr, ix)
// Did the handler wipe old messages? It will only do so if the user is // Did the handler wipe old messages? It will only do so if the user is
// scrolled to the bottom. // scrolled to the bottom.
if c.ListContainer.CleanMessages() { if c.ListContainer.CleanMessages() {
// We need to uncollapse the first (top) message. No length check is // We need to uncollapse the first (top) message. No length check is
// needed here, as we just inserted a message. // needed here, as we just inserted a message.
c.uncompact(c.FirstMessage()) c.uncompact(container.FirstMessage(c))
} }
// If we've prepended the message, then see if we need to collapse the // If we've prepended the message, then see if we need to collapse the
// second message. // second message.
if first := c.ListContainer.FirstMessage(); first != nil { if ix == -1 {
firstState := first.Unwrap(false) // If the author is the same, then collapse.
msgState := msgr.Unwrap(false) if sec := c.NthMessage(1); isCollapsible(sec, msgr.Unwrap()) {
c.compact(sec)
if firstState.ID == msgState.ID {
// If the author is the same, then collapse.
if sec := c.NthMessage(1); isCollapsible(sec, firstState) {
c.compact(sec)
}
} }
} }
} }
@ -152,10 +127,10 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
return return
} }
msgHeader := msg.Unwrap(false) msgHeader := msg.Unwrap()
prevHeader := prev.Unwrap(false) prevHeader := prev.Unwrap()
nextHeader := next.Unwrap(false) nextHeader := next.Unwrap()
// Check if the last message is the author's (relative to i): // Check if the last message is the author's (relative to i):
if prevHeader.Author.ID == msgHeader.Author.ID { if prevHeader.Author.ID == msgHeader.Author.ID {
@ -176,11 +151,21 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
} }
func (c *Container) uncompact(msg container.MessageRow) { func (c *Container) uncompact(msg container.MessageRow) {
full := WrapFullMessage(msg.Unwrap(true)) _, isFull := msg.(full)
if isFull {
return
}
full := WrapFullMessage(msg.Revert())
c.ListStore.SwapMessage(full) c.ListStore.SwapMessage(full)
} }
func (c *Container) compact(msg container.MessageRow) { func (c *Container) compact(msg container.MessageRow) {
compact := WrapCollapsedMessage(msg.Unwrap(true)) _, isCollapsed := msg.(collapsed)
if isCollapsed {
return
}
compact := WrapCollapsedMessage(msg.Revert())
c.ListStore.SwapMessage(compact) c.ListStore.SwapMessage(compact)
} }

View File

@ -25,7 +25,7 @@ func WrapCollapsedMessage(gc *message.State) *CollapsedMessage {
ts.SetMarginStart(container.ColumnSpacing * 2) ts.SetMarginStart(container.ColumnSpacing * 2)
// Set Content's padding accordingly to FullMessage's main box. // Set Content's padding accordingly to FullMessage's main box.
gc.Content.ToWidget().SetMarginEnd(container.ColumnSpacing * 2) gc.Content.SetMarginEnd(container.ColumnSpacing * 2)
gc.PackStart(ts, false, false, 0) gc.PackStart(ts, false, false, 0)
gc.PackStart(gc.Content, true, true, 0) gc.PackStart(gc.Content, true, true, 0)
@ -37,18 +37,19 @@ func WrapCollapsedMessage(gc *message.State) *CollapsedMessage {
} }
} }
func (c *CollapsedMessage) Collapsed() bool { return true } func (c *CollapsedMessage) Revert() *message.State {
c.ClearBox()
func (c *CollapsedMessage) Unwrap(revert bool) *message.State { c.Content.SetMarginEnd(0)
if revert { c.Timestamp.Destroy()
// Remove State's widgets from the containers. return c.Unwrap()
c.Remove(c.Timestamp)
c.Remove(c.Content)
}
return c.State
} }
type collapsed interface {
collapsed()
}
func (c *CollapsedMessage) collapsed() {}
type CollapsedSendingMessage struct { type CollapsedSendingMessage struct {
*CollapsedMessage *CollapsedMessage
message.Presender message.Presender

View File

@ -14,9 +14,6 @@ import (
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
// TopFullMargin is the margin on top of every full message.
const TopFullMargin = 4
type FullMessage struct { type FullMessage struct {
*message.State *message.State
@ -37,12 +34,22 @@ var (
) )
var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", ` var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", `
.cozy-avatar {
margin-top: 2px;
}
/* Slightly dip down on click */ /* Slightly dip down on click */
.cozy-avatar:active { .cozy-avatar:active {
margin-top: 1px; margin-top: 1px;
} }
`) `)
var mainCSS = primitives.PrepareClassCSS("cozy-main", `
.cozy-main {
margin-top: 4px;
}
`)
func NewFullMessage(msg cchat.MessageCreate) *FullMessage { func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
return WrapFullMessage(message.NewState(msg)) return WrapFullMessage(message.NewState(msg))
} }
@ -54,7 +61,6 @@ func WrapFullMessage(gc *message.State) *FullMessage {
header.Show() header.Show()
avatar := NewAvatar(gc.Row) avatar := NewAvatar(gc.Row)
avatar.SetMarginTop(TopFullMargin / 2)
avatar.SetMarginStart(container.ColumnSpacing * 2) avatar.SetMarginStart(container.ColumnSpacing * 2)
avatar.Connect("clicked", func(w gtk.IWidget) { avatar.Connect("clicked", func(w gtk.IWidget) {
if output := header.Output(); len(output.Mentions) > 0 { if output := header.Output(); len(output.Mentions) > 0 {
@ -72,7 +78,6 @@ func WrapFullMessage(gc *message.State) *FullMessage {
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
main.PackStart(header, false, false, 0) main.PackStart(header, false, false, 0)
main.PackStart(gc.Content, false, false, 0) main.PackStart(gc.Content, false, false, 0)
main.SetMarginTop(TopFullMargin)
main.SetMarginEnd(container.ColumnSpacing * 2) main.SetMarginEnd(container.ColumnSpacing * 2)
main.SetMarginStart(container.ColumnSpacing) main.SetMarginStart(container.ColumnSpacing)
main.Show() main.Show()
@ -84,6 +89,11 @@ func WrapFullMessage(gc *message.State) *FullMessage {
gc.PackStart(main, true, true, 0) gc.PackStart(main, true, true, 0)
gc.SetClass("cozy-full") gc.SetClass("cozy-full")
removeUpdate := gc.Author.Name.OnUpdate(func() {
avatar.SetImage(gc.Author.Name.Image())
header.SetLabel(gc.Author.Name.Label())
})
msg := &FullMessage{ msg := &FullMessage{
State: gc, State: gc,
timestamp: formatLongTime(gc.Time), timestamp: formatLongTime(gc.Time),
@ -92,17 +102,14 @@ func WrapFullMessage(gc *message.State) *FullMessage {
MainBox: main, MainBox: main,
HeaderLabel: header, HeaderLabel: header,
unwrap: gc.Author.Name.OnUpdate(func() { unwrap: func() { removeUpdate() },
avatar.SetImage(gc.Author.Name.Image())
header.SetLabel(gc.Author.Name.Label())
}),
} }
header.SetRenderer(func(rich text.Rich) markup.RenderOutput { cfg := markup.RenderConfig{}
cfg := markup.RenderConfig{} cfg.NoReferencing = true
cfg.NoReferencing = true cfg.SetForegroundAnchor(gc.ContentBodyStyle)
cfg.SetForegroundAnchor(gc.ContentBodyStyle)
header.SetRenderer(func(rich text.Rich) markup.RenderOutput {
output := markup.RenderCmplxWithConfig(rich, cfg) output := markup.RenderCmplxWithConfig(rich, cfg)
output.Markup = `<span font_weight="600">` + output.Markup + "</span>" output.Markup = `<span font_weight="600">` + output.Markup + "</span>"
output.Markup += msg.timestamp output.Markup += msg.timestamp
@ -113,27 +120,30 @@ func WrapFullMessage(gc *message.State) *FullMessage {
return msg return msg
} }
func (m *FullMessage) Collapsed() bool { return false } func (m *FullMessage) Revert() *message.State {
// Remove the handlers.
m.unwrap()
func (m *FullMessage) Unwrap(revert bool) *message.State { // Destroy the bottom leaf widgets first.
if revert { m.Avatar.Destroy()
// Remove the handlers. m.HeaderLabel.Destroy()
m.unwrap()
// Remove State's widgets from the containers. // Remove the content label from main then destroy it, in case destroying it
m.HeaderLabel.Destroy() // ruins the label.
m.MainBox.Remove(m.Content) // not ours, so don't destroy. m.MainBox.Remove(m.Content)
m.MainBox.Destroy()
// Remove the message from the grid. m.ClearBox()
m.Avatar.Destroy()
m.MainBox.Destroy()
}
return m.State return m.Unwrap()
} }
type full interface{ full() }
func (m *FullMessage) full() {}
func formatLongTime(t time.Time) string { func formatLongTime(t time.Time) string {
return `<span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>` return ` <span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>`
} }
type FullSendingMessage struct { type FullSendingMessage struct {

View File

@ -1,6 +1,7 @@
package container package container
import ( import (
"log"
"strings" "strings"
"time" "time"
@ -21,6 +22,19 @@ type messageKey struct {
func nonceKey(nonce string) messageKey { return messageKey{nonce, true} } func nonceKey(nonce string) messageKey { return messageKey{nonce, true} }
func idKey(id cchat.ID) messageKey { return messageKey{id, false} } func idKey(id cchat.ID) messageKey { return messageKey{id, false} }
// newKey creates a new message key.
func newKey(state *message.State) messageKey {
if state.ID != "" {
return messageKey{state.ID, false}
}
if state.Nonce != "" {
return messageKey{state.Nonce, true}
}
log.Printf("state is missing both ID and Nonce: \n%#v\n", state)
return messageKey{}
}
func parseKeyFromNamer(n primitives.Namer) messageKey { func parseKeyFromNamer(n primitives.Namer) messageKey {
name, err := n.GetName() name, err := n.GetName()
if err != nil { if err != nil {
@ -38,7 +52,7 @@ func parseKeyFromNamer(n primitives.Namer) messageKey {
case "nonce": case "nonce":
return messageKey{id: parts[1], nonce: true} return messageKey{id: parts[1], nonce: true}
default: default:
panic("Unknown prefix in row name " + parts[0]) panic("unknown prefix in message row name " + parts[0])
} }
} }
@ -123,9 +137,7 @@ func (c *ListStore) Reset() {
// Delegate removing children to the constructor. // Delegate removing children to the constructor.
c.messages = make(map[messageKey]*messageRow, BacklogLimit+1) c.messages = make(map[messageKey]*messageRow, BacklogLimit+1)
if c.self.ID != "" { c.self.Name.Stop()
c.self.Name.Stop()
}
} }
// SetSelf sets the current author to presend. If ID is empty or Namer is nil, // SetSelf sets the current author to presend. If ID is empty or Namer is nil,
@ -149,32 +161,38 @@ func (c *ListStore) MessagesLen() int {
// TODO: combine compact and full so they share the same attach method. // TODO: combine compact and full so they share the same attach method.
func (c *ListStore) SwapMessage(msg MessageRow) bool { func (c *ListStore) SwapMessage(msg MessageRow) bool {
// Unwrap msg from a *messageRow if it's not already. // Unwrap msg from a *messageRow if it's not already.
m, ok := msg.(*messageRow) if mrow, ok := msg.(*messageRow); ok {
if ok { msg = mrow.MessageRow
msg = m.MessageRow
} }
msgState := msg.Unwrap(false) state := msg.Unwrap()
// Get the current message's index. // Get the current message's index.
oldMsg, ix := c.findIndex(msgState.ID) oldMsg, ix := c.findIndex(state.ID)
if ix == -1 { if ix == -1 {
return false return false
} }
oldState := oldMsg.Unwrap(false) // Remove the previous message off the message map using the key from its
// state.
delete(c.messages, newKey(oldMsg.state))
// Remove the to-be-replaced message box. We should probably reuse the row. // Remove the to-be-replaced message box.
c.ListBox.Remove(oldState.Row) // TODO: We should probably reuse the row.
c.ListBox.Remove(oldMsg.state.Row)
// Add a row at index. The actual row we want to delete will be shifted // Add a row at index. The actual row we want to delete will be shifted
// downwards. // downwards.
c.ListBox.Insert(msgState.Row, ix) c.ListBox.Insert(state.Row, ix)
row := messageRow{
MessageRow: msg,
state: state,
}
// Set the message into the map. // Set the message into the map.
row := c.messages[idKey(msgState.ID)] c.messages[newKey(state)] = &row
row.MessageRow = msg c.bindMessage(&row)
c.bindMessage(row)
return true return true
} }
@ -241,6 +259,15 @@ func (c *ListStore) findIndex(findID cchat.ID) (found *messageRow, index int) {
return return
} }
// Fast path interface.
type messageFinder interface {
findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int)
}
var _ messageFinder = (*ListStore)(nil)
// findMessage finds a message with the given callback as the filter. If presend
// is false, then presend messages are ignored.
func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int) { func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int) {
var r *messageRow var r *messageRow
var i = c.MessagesLen() - 1 var i = c.MessagesLen() - 1
@ -251,7 +278,7 @@ func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messa
// If gridMsg is actually nil, then we have bigger issues. // If gridMsg is actually nil, then we have bigger issues.
if gridMsg != nil { if gridMsg != nil {
// Ignore sending messages. // Ignore sending messages if presend is false.
if (presend || gridMsg.presend == nil) && fn(gridMsg) { if (presend || gridMsg.presend == nil) && fn(gridMsg) {
r = gridMsg r = gridMsg
return true return true
@ -271,12 +298,12 @@ func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messa
} }
// FindMessage iterates backwards and returns the message if isMessage() returns // FindMessage iterates backwards and returns the message if isMessage() returns
// true on that message. It does not search presend messages. // true on that message.
func (c *ListStore) FindMessage(isMessage func(MessageRow) bool) MessageRow { func (c *ListStore) FindMessage(isMessage func(MessageRow) bool) (MessageRow, int) {
msg, _ := c.findMessage(false, func(row *messageRow) bool { msg, ix := c.findMessage(true, func(row *messageRow) bool {
return isMessage(row.MessageRow) return isMessage(row.MessageRow)
}) })
return unwrapRow(msg) return unwrapRow(msg), ix
} }
func (c *ListStore) nthMessage(n int) *messageRow { func (c *ListStore) nthMessage(n int) *messageRow {
@ -294,16 +321,6 @@ func (c *ListStore) NthMessage(n int) MessageRow {
return unwrapRow(c.nthMessage(n)) return unwrapRow(c.nthMessage(n))
} }
// FirstMessage returns the first message.
func (c *ListStore) FirstMessage() MessageRow {
return c.NthMessage(0)
}
// LastMessage returns the latest message.
func (c *ListStore) LastMessage() MessageRow {
return c.NthMessage(c.MessagesLen() - 1)
}
// Message finds the message state in the container. It is not thread-safe. This // Message finds the message state in the container. It is not thread-safe. This
// exists for backwards compatibility. // exists for backwards compatibility.
func (c *ListStore) Message(msgID cchat.ID, nonce string) MessageRow { func (c *ListStore) Message(msgID cchat.ID, nonce string) MessageRow {
@ -343,26 +360,24 @@ func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
} }
func (c *ListStore) bindMessage(msgc *messageRow) { func (c *ListStore) bindMessage(msgc *messageRow) {
state := msgc.Unwrap(false)
// Bind the message ID to the row so we can easily do a lookup. // Bind the message ID to the row so we can easily do a lookup.
key := messageKey{ key := messageKey{
id: state.ID, id: msgc.state.ID,
} }
if state.Nonce != "" { if msgc.state.Nonce != "" {
key.id = state.Nonce key.id = msgc.state.Nonce
key.nonce = true key.nonce = true
} }
state.Row.SetName(key.name()) msgc.state.Row.SetName(key.name())
msgc.MessageRow.SetReferenceHighlighter(c) msgc.MessageRow.SetReferenceHighlighter(c)
c.Controller.BindMenu(msgc.MessageRow) c.Controller.BindMenu(msgc.MessageRow)
} }
func (c *ListStore) AddMessage(msg MessageRow) { func (c *ListStore) AddMessageAt(msg MessageRow, ix int) {
state := msg.Unwrap(false) state := msg.Unwrap()
defer c.Controller.AuthorEvent(state.Author.ID) defer c.Controller.AuthorEvent(state.Author.ID)
@ -373,37 +388,34 @@ func (c *ListStore) AddMessage(msg MessageRow) {
return return
} }
// Iterate and compare timestamp to find where to insert a message. Note // Attempt to guess if this is a presend message or not. This should be
// that "before" is the message that will go before the to-be-inserted // unwrapped once it's finalized.
// method. presend, _ := msg.(message.Presender)
before, index := c.findMessage(true, func(before *messageRow) bool {
return before.Unwrap(false).Time.After(state.Time)
})
msgc := &messageRow{ msgc := &messageRow{
MessageRow: msg, MessageRow: msg,
presend: presend,
state: state,
} }
// Add the message. If before is nil, then the to-be-inserted message is the // Add the message. If before is nil, then the to-be-inserted message is the
// earliest message, therefore we prepend it. // earliest message, therefore we prepend it.
if before == nil { if ix < 0 {
index = 0 ix = 0
c.ListBox.Prepend(state.Row) c.ListBox.Prepend(state.Row)
} else { } else {
index++ // insert right after ix++ // insert right after
// Fast path: Insert did appear a lot on profiles, so we can try and use // Fast path: Insert did appear a lot on profiles, so we can try and use
// Add over Insert when we know. // Add over Insert when we know.
if c.MessagesLen() == index { if c.MessagesLen() == ix {
c.ListBox.Add(state.Row) c.ListBox.Add(state.Row)
} else { } else {
c.ListBox.Insert(state.Row, index) c.ListBox.Insert(state.Row, ix)
} }
} }
// Set the ID into the message map. c.messages[newKey(state)] = msgc
c.messages[idKey(state.ID)] = msgc
c.bindMessage(msgc) c.bindMessage(msgc)
} }
@ -436,19 +448,14 @@ func (c *ListStore) DeleteEarliest(n int) {
// after deleting, so we have to call Next manually before Removing. // after deleting, so we have to call Next manually before Removing.
primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) { primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) {
id := parseKeyFromNamer(v.(primitives.Namer)) id := parseKeyFromNamer(v.(primitives.Namer))
gridMsg := c.message(id.expand())
state := gridMsg.Unwrap(false) mr, ok := c.messages[id]
if !ok {
if state.ID != "" { log.Panicln("message with ID", id, "not found in map")
delete(c.messages, idKey(state.ID))
} }
if state.Nonce != "" { delete(c.messages, id)
delete(c.messages, nonceKey(state.Nonce)) destroyMsg(mr)
}
destroyMsg(gridMsg)
n-- n--
return n == 0 return n == 0
@ -464,7 +471,7 @@ func (c *ListStore) HighlightReference(ref markup.ReferenceSegment) {
func (c *ListStore) Highlight(msg MessageRow) { func (c *ListStore) Highlight(msg MessageRow) {
gts.ExecAsync(func() { gts.ExecAsync(func() {
state := msg.Unwrap(false) state := msg.Unwrap()
state.Row.GrabFocus() state.Row.GrabFocus()
c.ListBox.DragHighlightRow(state.Row) c.ListBox.DragHighlightRow(state.Row)
gts.DoAfter(2*time.Second, c.ListBox.DragUnhighlightRow) gts.DoAfter(2*time.Second, c.ListBox.DragUnhighlightRow)
@ -472,7 +479,6 @@ func (c *ListStore) Highlight(msg MessageRow) {
} }
func destroyMsg(row *messageRow) { func destroyMsg(row *messageRow) {
state := row.Unwrap(true) row.state.Author.Name.Stop()
state.Author.Name.Stop() row.state.Row.Destroy()
state.Row.Destroy()
} }

View File

@ -54,7 +54,7 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
return false return false
} }
id := msgr.Unwrap(false).ID id := msgr.Unwrap().ID
// If we don't support message editing, then passthrough events. // If we don't support message editing, then passthrough events.
if !f.Editable(id) { if !f.Editable(id) {

View File

@ -103,19 +103,19 @@ func (u *Container) shouldReveal() bool {
func (u *Container) Reset() { func (u *Container) Reset() {
u.SetRevealChild(false) u.SetRevealChild(false)
u.State.ID = ""
u.State.Name.Stop() u.State.Name.Stop()
} }
// Update is not thread-safe. // Update is not thread-safe.
func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) { func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) {
// Set the fallback username. u.State.ID = session.ID()
u.State.Name.BindNamer(u.main, "destroy", session)
// Reveal the name if it's not empty.
u.SetRevealChild(true) u.SetRevealChild(true)
// Does messenger implement Nicknamer? If yes, use it.
if nicknamer := messenger.AsNicknamer(); nicknamer != nil { if nicknamer := messenger.AsNicknamer(); nicknamer != nil {
u.State.Name.BindNamer(u.main, "destroy", nicknamer) u.State.Name.BindNamer(u.main, "destroy", nicknamer)
} else {
u.State.Name.BindNamer(u.main, "destroy", session)
} }
} }

View File

@ -238,11 +238,12 @@ func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
section.Box.PackStart(section.Body, false, false, 0) section.Box.PackStart(section.Body, false, false, 0)
section.Box.Show() section.Box.Show()
var members = map[string]*Member{} members := map[string]*Member{}
// On row click, show the mention popup if any. // On row click, show the mention popup if any.
section.Body.Connect("row-activated", func(_ *gtk.ListBox, r *gtk.ListBoxRow) { section.Body.Connect("row-activated", func(_ *gtk.ListBox, r *gtk.ListBoxRow) {
var i = r.GetIndex() i := r.GetIndex()
// Cold path; we can afford searching in the map. // Cold path; we can afford searching in the map.
for _, member := range members { for _, member := range members {
if member.ListBoxRow.GetIndex() == i { if member.ListBoxRow.GetIndex() == i {
@ -253,6 +254,7 @@ func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
section.name.QueueNamer(context.Background(), sect) section.name.QueueNamer(context.Background(), sect)
section.Header.Connect("destroy", section.name.Stop) section.Header.Connect("destroy", section.name.Stop)
section.Members = members
return section return section
} }
@ -328,12 +330,30 @@ var memberBoxCSS = primitives.PrepareClassCSS("member-box", `
} }
`) `)
var avatarMemberCSS = primitives.PrepareClassCSS("avatar-member", ` var avatarBoxMemberCSS = primitives.PrepareClassCSS("avatar-box-member", `
.avatar-member { .avatar-box-member {
padding-right: 10px; margin-right: 10px;
padding: 2px;
border: 1.5px solid;
border-color: #747F8D; /* Offline Grey */
border-radius: 99px;
}
.avatar-box-member.online {
border-color: #43B581;
}
.avatar-box-member.busy {
border-color: #F04747;
}
.avatar-box-member.idle {
border-color: #FAA61A;
} }
`) `)
var labelMemberCSS = primitives.PrepareClassCSS("label-member", ``)
func NewMember(member cchat.ListMember) *Member { func NewMember(member cchat.ListMember) *Member {
m := Member{} m := Member{}
@ -341,33 +361,44 @@ func NewMember(member cchat.ListMember) *Member {
evb.AddEvents(int(gdk.EVENT_ENTER_NOTIFY) | int(gdk.EVENT_LEAVE_NOTIFY)) evb.AddEvents(int(gdk.EVENT_ENTER_NOTIFY) | int(gdk.EVENT_LEAVE_NOTIFY))
evb.Show() evb.Show()
m.Avatar = roundimage.NewStillImage(evb, 9999) m.Avatar = roundimage.NewStillImage(evb, 0)
m.Avatar.SetSize(AvatarSize) m.Avatar.SetSize(AvatarSize)
m.Avatar.SetPlaceholderIcon("user-info-symbolic", AvatarSize) m.Avatar.SetPlaceholderIcon("user-info-symbolic", AvatarSize)
m.Avatar.Show() m.Avatar.Show()
avatarMemberCSS(m.Avatar)
rich.BindRoundImage(m.Avatar, &m.name, true) rich.BindRoundImage(m.Avatar, &m.name, true)
avaBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
avaBox.SetVAlign(gtk.ALIGN_CENTER)
avaBox.PackStart(m.Avatar, false, false, 0)
avaBox.Show()
avatarBoxMemberCSS(avaBox)
m.Name = rich.NewLabel(&m.name) m.Name = rich.NewLabel(&m.name)
m.Name.SetUseMarkup(true) m.Name.SetUseMarkup(true)
m.Name.SetXAlign(0) m.Name.SetXAlign(0)
m.Name.SetEllipsize(pango.ELLIPSIZE_END) m.Name.SetEllipsize(pango.ELLIPSIZE_END)
m.Name.Show() m.Name.Show()
labelMemberCSS(m.Name)
// Keep track of the current status class to replace.
var statusClass string
styler, _ := avaBox.GetStyleContext()
m.Name.SetRenderer(func(rich text.Rich) markup.RenderOutput { m.Name.SetRenderer(func(rich text.Rich) markup.RenderOutput {
out := markup.RenderCmplx(rich) out := markup.RenderCmplxWithConfig(rich, markup.RenderConfig{
NoMentionLinks: true,
})
if m.status != cchat.StatusUnknown { if statusClass != "" {
out.Markup = fmt.Sprintf( styler.RemoveClass(statusClass)
`<span color="#%06X" size="large">●</span> %s`,
statusColors(member.Status()), out.Markup,
)
} }
statusClass = statusClassName(m.status)
styler.AddClass(statusClass)
if !m.second.IsEmpty() { if !m.second.IsEmpty() {
out.Markup += fmt.Sprintf( out.Markup += fmt.Sprintf(
"\n"+`<span alpha="85%%"><sup>%s</sup></span>`, `<span alpha="85%%" size="small">`+"\n"+`%s</span>`,
markup.Render(m.second), markup.Render(m.second),
) )
} }
@ -376,7 +407,7 @@ func NewMember(member cchat.ListMember) *Member {
}) })
m.Main, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) m.Main, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
m.Main.PackStart(m.Avatar, false, false, 0) m.Main.PackStart(avaBox, false, false, 0)
m.Main.PackStart(m.Name, true, true, 0) m.Main.PackStart(m.Name, true, true, 0)
m.Main.Show() m.Main.Show()
memberBoxCSS(m.Main) memberBoxCSS(m.Main)
@ -392,6 +423,25 @@ func NewMember(member cchat.ListMember) *Member {
return &m return &m
} }
func statusClassName(status cchat.Status) string {
switch status {
case cchat.StatusOnline:
return "online"
case cchat.StatusBusy:
return "busy"
case cchat.StatusAway:
fallthrough
case cchat.StatusIdle:
return "idle"
case cchat.StatusInvisible:
fallthrough
case cchat.StatusOffline:
fallthrough
default:
return ""
}
}
var noMentionLinks = markup.RenderConfig{ var noMentionLinks = markup.RenderConfig{
NoMentionLinks: true, NoMentionLinks: true,
NoReferencing: true, NoReferencing: true,

View File

@ -19,10 +19,11 @@ import (
// made for containers to override; methods not meant to be override are not // made for containers to override; methods not meant to be override are not
// exposed and will be done directly on the State. // exposed and will be done directly on the State.
type Container interface { type Container interface {
// Unwrap unwraps the message container and, if revert is true, revert the // Unwrap returns the internal message state.
// state to a clean version. Containers must implement this method by Unwrap() *State
// itself. // Revert unwraps and reverts all widget changes to the internal state then
Unwrap(revert bool) *State // returns that state.
Revert() *State
// UpdateContent updates the underlying content widget. // UpdateContent updates the underlying content widget.
UpdateContent(content text.Rich, edited bool) UpdateContent(content text.Rich, edited bool)
@ -50,8 +51,7 @@ type State struct {
MenuItems []menu.Item MenuItems []menu.Item
} }
// NewState creates a new message state with the given ID and nonce. It does not // NewState creates a new message state with the given MessageCreate.
// update the widgets, so FillContainer should be called afterwards.
func NewState(msg cchat.MessageCreate) *State { func NewState(msg cchat.MessageCreate) *State {
author := msg.Author() author := msg.Author()
@ -61,6 +61,7 @@ func NewState(msg cchat.MessageCreate) *State {
c.ID = msg.ID() c.ID = msg.ID()
c.Time = msg.Time() c.Time = msg.Time()
c.Nonce = msg.Nonce() c.Nonce = msg.Nonce()
c.UpdateContent(msg.Content(), false)
return c return c
} }
@ -69,8 +70,7 @@ func NewState(msg cchat.MessageCreate) *State {
// immediately afterwards; it is invalid once the state is used. // immediately afterwards; it is invalid once the state is used.
func NewEmptyState() *State { func NewEmptyState() *State {
ctbody := labeluri.NewLabel(text.Rich{}) ctbody := labeluri.NewLabel(text.Rich{})
ctbody.SetVExpand(true) ctbody.SetHAlign(gtk.ALIGN_FILL)
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)
@ -84,10 +84,11 @@ func NewEmptyState() *State {
// 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.SetHAlign(gtk.ALIGN_FILL)
ctbox.Show() ctbox.Show()
// Box that belongs to the implementations of messages.
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.Show() box.Show()
@ -122,13 +123,38 @@ func NewEmptyState() *State {
return gc return gc
} }
// ClearBox clears the state's widget container.
func (m *State) ClearBox() {
primitives.RemoveChildren(m)
m.SetClass("")
}
// // For debugging use only.
// func (m *State) PackStart(child gtk.IWidget, expand bool, fill bool, padding uint) {
// paths := make([]string, 0, 5)
// for i := 1; i < 5; i++ {
// _, file, line, ok := runtime.Caller(i)
// if !ok {
// break
// }
//
// paths = append(paths, fmt.Sprintf("%s:%d", filepath.Base(file), line))
// }
//
// log.Println("child packstart", m.ID, "at", strings.Join(paths, " < "))
// m.Box.PackStart(child, expand, fill, padding)
// }
// SetClass sets the internal row's class. // SetClass sets the internal row's class.
func (m *State) SetClass(class string) { func (m *State) SetClass(class string) {
if m.class != "" { if m.class != "" {
primitives.RemoveClass(m.Row, m.class) primitives.RemoveClass(m.Row, m.class)
} }
primitives.AddClass(m.Row, class) if class != "" {
primitives.AddClass(m.Row, class)
}
m.class = class m.class = class
} }
@ -153,3 +179,6 @@ func (m *State) UpdateContent(content text.Rich, edited bool) {
func (m *State) Focusable() gtk.IWidget { func (m *State) Focusable() gtk.IWidget {
return m.Content return m.Content
} }
// Unwrap returns itself.
func (m *State) Unwrap() *State { return m }

View File

@ -45,14 +45,10 @@ type PresendState struct {
uploads *attachment.MessageUploader uploads *attachment.MessageUploader
} }
var ( var _ Presender = (*PresendState)(nil)
_ Presender = (*PresendState)(nil)
)
type SendMessageData struct { // NewPresendState creates a new presend state. The caller must call one of the
} // state setters, usually SetLoading.
// NewPresendState creates a new presend state.
func NewPresendState(self *Author, msg PresendMessage) *PresendState { func NewPresendState(self *Author, msg PresendMessage) *PresendState {
c := NewEmptyState() c := NewEmptyState()
c.Author = self c.Author = self
@ -64,7 +60,7 @@ func NewPresendState(self *Author, msg PresendMessage) *PresendState {
presend: msg, presend: msg,
uploads: attachment.NewMessageUploader(msg.Files()), uploads: attachment.NewMessageUploader(msg.Files()),
} }
p.SetLoading() // p.SetLoading()
return p return p
} }

View File

@ -84,7 +84,7 @@ func (mc *MessageControl) Enable(msg container.MessageRow, names MessageItemName
mc.SetSensitive(true) mc.SetSensitive(true)
mc.SetRevealChild(true && !mc.hide) mc.SetRevealChild(true && !mc.hide)
unwrap := msg.Unwrap(false) unwrap := msg.Unwrap()
mc.Reply.bind(menu.FindItemFunc(unwrap.MenuItems, names.Reply)) mc.Reply.bind(menu.FindItemFunc(unwrap.MenuItems, names.Reply))
mc.Edit.bind(menu.FindItemFunc(unwrap.MenuItems, names.Edit)) mc.Edit.bind(menu.FindItemFunc(unwrap.MenuItems, names.Edit))

View File

@ -114,9 +114,10 @@ func NewView(c Controller) *View {
view.MsgBox.Show() view.MsgBox.Show()
view.Scroller = autoscroll.NewScrolledWindow() view.Scroller = autoscroll.NewScrolledWindow()
view.Scroller.Add(view.MsgBox) view.Scroller.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
view.Scroller.SetVExpand(true) view.Scroller.SetVExpand(true)
view.Scroller.SetHExpand(true) view.Scroller.SetHExpand(true)
view.Scroller.Add(view.MsgBox)
view.Scroller.Show() view.Scroller.Show()
messageScroller(view.Scroller) messageScroller(view.Scroller)
@ -352,12 +353,12 @@ func (v *View) JoinServer(ses *session.Row, srv *server.ServerRow, bc traverse.B
} }
func (v *View) FetchBacklog() { func (v *View) FetchBacklog() {
var backlogger = v.state.Backlogger() backlogger := v.state.Backlogger()
if backlogger == nil { if backlogger == nil {
return return
} }
var firstMsg = v.Container.FirstMessage() firstMsg := container.FirstMessage(v.Container)
if firstMsg == nil { if firstMsg == nil {
return return
} }
@ -365,12 +366,12 @@ func (v *View) FetchBacklog() {
// Set the window as busy. TODO: loading circles. // Set the window as busy. TODO: loading circles.
v.ctrl.OnMessageBusy() v.ctrl.OnMessageBusy()
var done = func() { done := func() {
v.ctrl.OnMessageDone() v.ctrl.OnMessageDone()
v.Container.Highlight(firstMsg) v.Container.Highlight(firstMsg)
} }
firstID := firstMsg.Unwrap(false).ID firstID := firstMsg.Unwrap().ID
gts.Async(func() (func(), error) { gts.Async(func() (func(), error) {
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second) ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
@ -396,59 +397,62 @@ func (v *View) MessageAuthor(msgID cchat.ID) *message.Author {
return nil return nil
} }
return msg.Unwrap(false).Author return msg.Unwrap().Author
} }
// Author returns the author from the message list with the given author ID. // Author returns the author from the message list with the given author ID.
func (v *View) Author(authorID cchat.ID) rich.LabelStateStorer { func (v *View) Author(authorID cchat.ID) rich.LabelStateStorer {
msg := v.Container.FindMessage(func(msg container.MessageRow) bool { msg, _ := v.Container.FindMessage(func(msg container.MessageRow) bool {
return msg.Unwrap(false).Author.ID == authorID return msg.Unwrap().Author.ID == authorID
}) })
if msg == nil { if msg == nil {
return nil return nil
} }
state := msg.Unwrap(false) state := msg.Unwrap()
return &state.Author.Name return &state.Author.Name
} }
// LatestMessageFrom returns the last message ID with that author. // LatestMessageFrom returns the last message ID with that author.
func (v *View) LatestMessageFrom(userID cchat.ID) container.MessageRow { func (v *View) LatestMessageFrom(userID cchat.ID) container.MessageRow {
return container.LatestMessageFrom(v.Container, userID) msg, _ := container.LatestMessageFrom(v.Container, userID)
return msg
} }
func (v *View) SendMessage(msg message.PresendMessage) { func (v *View) SendMessage(msg message.PresendMessage) {
state := message.NewPresendState(v.InputView.Username.State, msg) state := message.NewPresendState(v.InputView.Username.State, msg)
msgr := v.Container.NewPresendMessage(state) msgr := v.Container.NewPresendMessage(state)
v.retryMessage(state, msgr)
v.retryMessage(msgr)
} }
// retryMessage sends the message. // retryMessage sends the message.
func (v *View) retryMessage(presend container.PresendMessageRow) { func (v *View) retryMessage(state *message.PresendState, presend container.PresendMessageRow) {
var sender = v.InputView.Sender var sender = v.InputView.Sender
if sender == nil { if sender == nil {
return return
} }
// Ensure the message is set to loading.
presend.SetLoading()
go func() { go func() {
if err := sender.Send(presend.SendingMessage()); err != nil { err := sender.Send(presend.SendingMessage())
// Set the message's state to errored again, but we don't need to if err == nil {
// rebind the menu. return
gts.ExecAsync(func() {
// Set the retry message.
presend.SetSentError(err)
// Only attach the menu once. Further retries do not need to be
// reattached.
state := presend.Unwrap(false)
state.MenuItems = []menu.Item{
menu.SimpleItem("Retry", func() {
presend.SetLoading()
v.retryMessage(presend)
}),
}
})
} }
// Set the message's state to errored again, but we don't need to rebind
// the menu.
gts.ExecAsync(func() {
presend.SetSentError(err)
state.MenuItems = []menu.Item{
menu.SimpleItem("Retry", func() {
presend.SetLoading()
v.retryMessage(state, presend)
}),
}
})
}() }()
} }
@ -461,7 +465,7 @@ var messageItemNames = MessageItemNames{
// 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) {
state := msg.Unwrap(false) state := msg.Unwrap()
// Add 1 for the edit menu item. // Add 1 for the edit menu item.
var mitems = []menu.Item{ var mitems = []menu.Item{

View File

@ -173,17 +173,15 @@ func (i *Image) SetImageURLInto(url string, otherImage httputil.ImageContainer)
return return
} }
if i.icon.name != "" {
primitives.SetImageIcon(i, i.icon.name, i.icon.size)
goto noImage
}
if i.ifNone != nil { if i.ifNone != nil {
i.ifNone(ctx) i.ifNone(ctx)
return return
} }
noImage: if i.icon.name != "" {
primitives.SetImageIcon(i, i.icon.name, i.icon.size)
}
i.Image.SetFromPixbuf(nil) i.Image.SetFromPixbuf(nil)
i.cancel() i.cancel()
} }
@ -216,9 +214,14 @@ func (i *Image) drawer(image *gtk.Image, cc *cairo.Context) bool {
return false return false
} }
a := image.GetAllocation() var w, h float64
w := float64(a.GetWidth()) if reqW, reqH := image.GetSizeRequest(); reqW > 0 && reqH > 0 {
h := float64(a.GetHeight()) w = float64(reqW)
h = float64(reqH)
} else {
w = float64(image.GetAllocatedWidth())
h = float64(image.GetAllocatedHeight())
}
min := w min := w
// Use the largest side for radius calculation. // Use the largest side for radius calculation.

View File

@ -62,29 +62,23 @@ func (namec *NameContainer) Stop() {
if namec.state != nil { if namec.state != nil {
namec.state.Stop() namec.state.Stop()
namec.LabelState.setLabel(text.Plain("")) namec.LabelState.setLabel(text.Plain(""))
} else {
namec.state = &containerState{
current: func() {},
stop: func() {},
}
runtime.SetFinalizer(namec.state, (*containerState).Stop)
} }
} }
func (state *containerState) Stop() { func (state *containerState) Stop() {
if state.current != nil { state.current()
state.current() state.stop()
state.current = nil
}
if state.stop != nil {
state.stop()
state.stop = nil
}
} }
// QueueNamer tries using the namer in the background and queue the setter onto // QueueNamer tries using the namer in the background and queue the setter onto
// the next GLib loop iteration. // the next GLib loop iteration.
func (namec *NameContainer) QueueNamer(ctx context.Context, namer cchat.Namer) { func (namec *NameContainer) QueueNamer(ctx context.Context, namer cchat.Namer) {
if namec.state == nil {
namec.state = &containerState{}
runtime.SetFinalizer(namec.state, (*containerState).Stop)
}
namec.Stop() namec.Stop()
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
@ -98,7 +92,6 @@ func (namec *NameContainer) QueueNamer(ctx context.Context, namer cchat.Namer) {
gts.ExecAsync(func() { gts.ExecAsync(func() {
namec.state.current() namec.state.current()
namec.state.current = nil
namec.state.stop = stop namec.state.stop = stop
}) })
}() }()
@ -115,11 +108,11 @@ func (namec *NameContainer) BindNamer(w primitives.Connector, sig string, namer
// namec.Stop() // namec.Stop()
// ctx, cancel := context.WithCancel(context.Background()) // ctx, cancel := context.WithCancel(context.Background())
// namec.current = cancel // namec.state.current = cancel
// // TODO: this might leak, because namec.Stop references the fns list which // // TODO: this might leak, because namec.Stop references the fns list which
// // might reference w indirectly. // // might reference w indirectly.
// w.Connect(sig, namec.Stop) // handle := w.Connect(sig, namec.Stop)
// go func() { // go func() {
// stop, err := namer.Name(ctx, namec) // stop, err := namer.Name(ctx, namec)
@ -128,9 +121,18 @@ func (namec *NameContainer) BindNamer(w primitives.Connector, sig string, namer
// } // }
// gts.ExecAsync(func() { // gts.ExecAsync(func() {
// namec.current() // namec.state.current()
// namec.current = nil
// namec.stop = stop // nil is OK. // if stop != nil {
// namec.state.stop = func() {
// w.HandlerDisconnect(handle)
// stop()
// }
// } else {
// namec.state.stop = func() {
// w.HandlerDisconnect(handle)
// }
// }
// }) // })
// }() // }()
} }

View File

@ -124,17 +124,13 @@ func (h *Header) SetSessionMenu(s *session.Row) {
} }
type sizeBinder interface { type sizeBinder interface {
primitives.Connector gtk.IWidget
GetAllocatedWidth() int
} }
var _ sizeBinder = (*List)(nil) var _ sizeBinder = (*List)(nil)
func (h *Header) AppMenuBindSize(c sizeBinder) { func (h *Header) AppMenuBindSize(c sizeBinder) {
update := func(c sizeBinder) { sg, _ := gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL)
h.AppMenu.SetSizeRequest(c.GetAllocatedWidth(), -1) sg.AddWidget(c)
} sg.AddWidget(h.AppMenu)
c.Connect("size-allocate", update)
update(c)
} }

View File

@ -1,8 +1,6 @@
package session package session
import ( import (
"fmt"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/humanize" "github.com/diamondburned/cchat-gtk/internal/humanize"
@ -11,8 +9,8 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server" "github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse" "github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/serverpane" "github.com/diamondburned/cchat-gtk/internal/ui/service/session/serverpane"
"github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
) )
const FaceSize = 48 // gtk.ICON_SIZE_DIALOG const FaceSize = 48 // gtk.ICON_SIZE_DIALOG
@ -212,33 +210,20 @@ func (s *Servers) setFailed(err error) {
s.Stack.Remove(w) s.Stack.Remove(w)
} }
// Create a BLANK label for padding.
ltop, _ := gtk.LabelNew("")
ltop.Show()
// Create a retry button. // Create a retry button.
btn, _ := gtk.ButtonNewFromIconName("view-refresh-symbolic", gtk.ICON_SIZE_DIALOG) btn, _ := gtk.ButtonNewFromIconName("view-refresh-symbolic", gtk.ICON_SIZE_BUTTON)
btn.SetLabel("Retry")
btn.Connect("clicked", s.load)
btn.Show() btn.Show()
btn.Connect("clicked", func(interface{}) { s.load() })
// Create a bottom label for the error itself. page := handy.StatusPageNew()
lerr, _ := gtk.LabelNew("") page.SetTitle("Error")
lerr.SetSingleLineMode(true) page.SetIconName("dialog-error")
lerr.SetEllipsize(pango.ELLIPSIZE_MIDDLE) page.SetTooltipText(err.Error())
lerr.SetMarkup(fmt.Sprintf( page.SetDescription(humanize.Error(err))
`<span color="red"><b>Error:</b> %s</span>`, page.Add(btn)
humanize.Error(err),
))
lerr.Show()
// Add these items into the box. s.Stack.AddNamed(page, "error")
b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
b.PackStart(ltop, false, false, 0)
b.PackStart(btn, false, false, 10) // pad
b.PackStart(lerr, false, false, 0)
b.Show()
s.Stack.AddNamed(b, "error")
s.Stack.SetVisibleChildName("error") s.Stack.SetVisibleChildName("error")
} }

View File

@ -1,15 +0,0 @@
// Code generated by goprofiler. DO NOT EDIT.
package main
import (
"net/http"
_ "net/http/pprof"
)
func init() {
go func() {
println("Serving HTTP at 127.0.0.1:48574 for profiler at /debug/pprof")
panic(http.ListenAndServe("127.0.0.1:48574", nil))
}()
}

View File

@ -1,21 +1,19 @@
{ pkgs ? import <nixpkgs> {} }: { unstable ? import <unstable> {} }:
pkgs.stdenv.mkDerivation rec { unstable.stdenv.mkDerivation rec {
name = "cchat-gtk"; name = "cchat-gtk";
version = "0.0.2"; version = "0.0.2";
buildInputs = [ buildInputs = with unstable; [
pkgs.libhandy libhandy
pkgs.gnome3.gspell gnome3.gspell
pkgs.gnome3.glib gnome3.glib
pkgs.gnome3.gtk gnome3.gtk
]; ];
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with unstable; [
pkgconfig go pkgconfig
go
wrapGAppsHook
]; ];
# Debug flags.
CGO_CFLAGS = "-g";
CGO_CXXFLAGS = "-g";
} }