diff --git a/go.mod b/go.mod
index 9de96e5..a6ad11c 100644
--- a/go.mod
+++ b/go.mod
@@ -2,13 +2,11 @@ module github.com/diamondburned/cchat-gtk
go 1.16
-replace github.com/diamondburned/cchat-discord => ../cchat-discord
-
require (
github.com/Xuanwo/go-locale v1.0.0
github.com/alecthomas/chroma v0.7.3
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/handy v0.0.0-20210329054445-387ad28eb2c2
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
diff --git a/go.sum b/go.sum
index 1cb53fc..5910f7d 100644
--- a/go.sum
+++ b/go.sum
@@ -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-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-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/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=
diff --git a/internal/ui/config/config.go b/internal/ui/config/config.go
index 0f4f6bf..1cbe46f 100644
--- a/internal/ui/config/config.go
+++ b/internal/ui/config/config.go
@@ -96,8 +96,6 @@ func Restore() {
log.Error(errors.Wrap(err, "Failed to unmarshal main config.json"))
}
- log.Printlnf("To restore: %#v", toRestore)
-
for path, v := range toRestore {
if err := UnmarshalFromFile(path, v); err != nil {
log.Error(errors.Wrapf(err, "Failed to unmarshal %s", path))
diff --git a/internal/ui/messages/container/compact/compact.go b/internal/ui/messages/container/compact/compact.go
index e8e985b..08b2eb4 100644
--- a/internal/ui/messages/container/compact/compact.go
+++ b/internal/ui/messages/container/compact/compact.go
@@ -22,17 +22,23 @@ func NewContainer(ctrl container.Controller) *Container {
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
msg := WrapPresendMessage(state)
- c.AddMessage(msg)
+ c.addMessage(msg)
return msg
}
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
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) {
gts.ExecAsync(func() { container.UpdateMessage(c, msg) })
}
diff --git a/internal/ui/messages/container/compact/message.go b/internal/ui/messages/container/compact/message.go
index 8054cf4..8228198 100644
--- a/internal/ui/messages/container/compact/message.go
+++ b/internal/ui/messages/container/compact/message.go
@@ -94,13 +94,9 @@ func (m Message) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
m.Username.SetReferenceHighlighter(r)
}
-func (m Message) Unwrap(revert bool) *message.State {
- if revert {
- m.unwrap()
+func (m Message) Revert() *message.State {
+ m.unwrap()
+ m.ClearBox()
- primitives.RemoveChildren(m)
- m.SetClass("")
- }
-
- return m.State
+ return m.Unwrap()
}
diff --git a/internal/ui/messages/container/container.go b/internal/ui/messages/container/container.go
index a0722c8..6db3023 100644
--- a/internal/ui/messages/container/container.go
+++ b/internal/ui/messages/container/container.go
@@ -1,6 +1,8 @@
package container
import (
+ "time"
+
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"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(state *message.PresendState) PresendMessageRow
- // AddMessage adds a new message into the list.
- AddMessage(row MessageRow)
+ // AddMessageAt adds a new message into the list at the given index.
+ AddMessageAt(row MessageRow, ix int)
- // FirstMessage returns the first message in the buffer. Nil is returned if
- // there's nothing.
- FirstMessage() MessageRow
- // LastMessage returns the last message in the buffer or nil if there's
+ // MessagesLen returns the current number of messages.
+ MessagesLen() int
+ // NthMessage returns the nth message in the buffer or nil if there's
// nothing.
- LastMessage() MessageRow
+ NthMessage(ix int) MessageRow
+
// Message finds and returns the message, if any. It performs maximum 2
// constant-time lookups.
Message(id cchat.ID, nonce string) MessageRow
// FindMessage finds a message that satisfies the given callback. It
// 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(msg MessageRow)
@@ -71,10 +73,56 @@ func UpdateMessage(ct Container, update cchat.MessageUpdate) {
}
// LatestMessageFrom returns the latest message from the given author ID.
-func LatestMessageFrom(ct Container, authorID cchat.ID) MessageRow {
- return ct.FindMessage(func(msg MessageRow) bool {
- return msg.Unwrap(false).Author.ID == authorID
+func LatestMessageFrom(ct Container, authorID cchat.ID) (MessageRow, int) {
+ finder, ok := ct.(messageFinder)
+ 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.
@@ -109,6 +157,7 @@ type ListContainer struct {
// messageRow w/ required internals
type messageRow struct {
MessageRow
+ state *message.State
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
// bottom. True is returned if there were changes.
func (c *ListContainer) CleanMessages() bool {
diff --git a/internal/ui/messages/container/cozy/avatar.go b/internal/ui/messages/container/cozy/avatar.go
index cf89a68..3749b72 100644
--- a/internal/ui/messages/container/cozy/avatar.go
+++ b/internal/ui/messages/container/cozy/avatar.go
@@ -14,7 +14,7 @@ type Avatar struct {
}
func NewAvatar(parent primitives.Connector) *Avatar {
- img := roundimage.NewStillImage(nil, 0)
+ img := roundimage.NewStillImage(parent, 0)
img.SetSizeRequest(AvatarSize, AvatarSize)
img.Show()
diff --git a/internal/ui/messages/container/cozy/cozy.go b/internal/ui/messages/container/cozy/cozy.go
index bb542e5..825ba18 100644
--- a/internal/ui/messages/container/cozy/cozy.go
+++ b/internal/ui/messages/container/cozy/cozy.go
@@ -10,20 +10,6 @@ import (
"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 (
AvatarSize = 40
AvatarMargin = 10
@@ -61,69 +47,58 @@ func NewContainer(ctrl container.Controller) *Container {
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
// isCollapsible returns true if the given lastMsg has matching conditions with
// the given msg.
func isCollapsible(last container.MessageRow, msg *message.State) bool {
- if last == nil || msg.ID == "" {
+ if last == nil || msg == nil {
return false
}
- lastMsg := last.Unwrap(false)
+ lastMsg := last.Unwrap()
return true &&
- lastMsg.Author.ID == msg.ID &&
+ lastMsg.Author.ID == msg.Author.ID &&
lastMsg.Time.Add(splitDuration).After(msg.Time)
}
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
- msgr := NewPresendMessage(state, c.LastMessage())
- c.AddMessage(msgr)
+ before, at := container.InsertPosition(c, state.Time)
+ msgr := NewPresendMessage(state, before)
+ c.AddMessageAt(msgr, at)
return msgr
}
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
+ before, at := container.InsertPosition(c, msg.Time())
state := message.NewState(msg)
- msgr := NewMessage(state, c.LastMessage())
-
- c.AddMessage(msgr)
+ msgr := NewMessage(state, before)
+ c.AddMessageAt(msgr, at)
})
}
// 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
// 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
// scrolled to the bottom.
if c.ListContainer.CleanMessages() {
// We need to uncollapse the first (top) message. No length check is
// 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
// second message.
- if first := c.ListContainer.FirstMessage(); first != nil {
- firstState := first.Unwrap(false)
- msgState := msgr.Unwrap(false)
-
- if firstState.ID == msgState.ID {
- // If the author is the same, then collapse.
- if sec := c.NthMessage(1); isCollapsible(sec, firstState) {
- c.compact(sec)
- }
+ if ix == -1 {
+ // If the author is the same, then collapse.
+ if sec := c.NthMessage(1); isCollapsible(sec, msgr.Unwrap()) {
+ c.compact(sec)
}
}
}
@@ -152,10 +127,10 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
return
}
- msgHeader := msg.Unwrap(false)
+ msgHeader := msg.Unwrap()
- prevHeader := prev.Unwrap(false)
- nextHeader := next.Unwrap(false)
+ prevHeader := prev.Unwrap()
+ nextHeader := next.Unwrap()
// Check if the last message is the author's (relative to i):
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) {
- full := WrapFullMessage(msg.Unwrap(true))
+ _, isFull := msg.(full)
+ if isFull {
+ return
+ }
+
+ full := WrapFullMessage(msg.Revert())
c.ListStore.SwapMessage(full)
}
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)
}
diff --git a/internal/ui/messages/container/cozy/message_collapsed.go b/internal/ui/messages/container/cozy/message_collapsed.go
index 3562903..6e56f40 100644
--- a/internal/ui/messages/container/cozy/message_collapsed.go
+++ b/internal/ui/messages/container/cozy/message_collapsed.go
@@ -25,7 +25,7 @@ func WrapCollapsedMessage(gc *message.State) *CollapsedMessage {
ts.SetMarginStart(container.ColumnSpacing * 2)
// 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(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) Unwrap(revert bool) *message.State {
- if revert {
- // Remove State's widgets from the containers.
- c.Remove(c.Timestamp)
- c.Remove(c.Content)
- }
-
- return c.State
+func (c *CollapsedMessage) Revert() *message.State {
+ c.ClearBox()
+ c.Content.SetMarginEnd(0)
+ c.Timestamp.Destroy()
+ return c.Unwrap()
}
+type collapsed interface {
+ collapsed()
+}
+
+func (c *CollapsedMessage) collapsed() {}
+
type CollapsedSendingMessage struct {
*CollapsedMessage
message.Presender
diff --git a/internal/ui/messages/container/cozy/message_full.go b/internal/ui/messages/container/cozy/message_full.go
index 9506755..2110bd1 100644
--- a/internal/ui/messages/container/cozy/message_full.go
+++ b/internal/ui/messages/container/cozy/message_full.go
@@ -14,9 +14,6 @@ import (
"github.com/gotk3/gotk3/gtk"
)
-// TopFullMargin is the margin on top of every full message.
-const TopFullMargin = 4
-
type FullMessage struct {
*message.State
@@ -37,12 +34,22 @@ var (
)
var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", `
+ .cozy-avatar {
+ margin-top: 2px;
+ }
+
/* Slightly dip down on click */
.cozy-avatar:active {
margin-top: 1px;
}
`)
+var mainCSS = primitives.PrepareClassCSS("cozy-main", `
+ .cozy-main {
+ margin-top: 4px;
+ }
+`)
+
func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
return WrapFullMessage(message.NewState(msg))
}
@@ -54,7 +61,6 @@ func WrapFullMessage(gc *message.State) *FullMessage {
header.Show()
avatar := NewAvatar(gc.Row)
- avatar.SetMarginTop(TopFullMargin / 2)
avatar.SetMarginStart(container.ColumnSpacing * 2)
avatar.Connect("clicked", func(w gtk.IWidget) {
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.PackStart(header, false, false, 0)
main.PackStart(gc.Content, false, false, 0)
- main.SetMarginTop(TopFullMargin)
main.SetMarginEnd(container.ColumnSpacing * 2)
main.SetMarginStart(container.ColumnSpacing)
main.Show()
@@ -84,6 +89,11 @@ func WrapFullMessage(gc *message.State) *FullMessage {
gc.PackStart(main, true, true, 0)
gc.SetClass("cozy-full")
+ removeUpdate := gc.Author.Name.OnUpdate(func() {
+ avatar.SetImage(gc.Author.Name.Image())
+ header.SetLabel(gc.Author.Name.Label())
+ })
+
msg := &FullMessage{
State: gc,
timestamp: formatLongTime(gc.Time),
@@ -92,17 +102,14 @@ func WrapFullMessage(gc *message.State) *FullMessage {
MainBox: main,
HeaderLabel: header,
- unwrap: gc.Author.Name.OnUpdate(func() {
- avatar.SetImage(gc.Author.Name.Image())
- header.SetLabel(gc.Author.Name.Label())
- }),
+ unwrap: func() { removeUpdate() },
}
- header.SetRenderer(func(rich text.Rich) markup.RenderOutput {
- cfg := markup.RenderConfig{}
- cfg.NoReferencing = true
- cfg.SetForegroundAnchor(gc.ContentBodyStyle)
+ cfg := markup.RenderConfig{}
+ cfg.NoReferencing = true
+ cfg.SetForegroundAnchor(gc.ContentBodyStyle)
+ header.SetRenderer(func(rich text.Rich) markup.RenderOutput {
output := markup.RenderCmplxWithConfig(rich, cfg)
output.Markup = `` + output.Markup + ""
output.Markup += msg.timestamp
@@ -113,27 +120,30 @@ func WrapFullMessage(gc *message.State) *FullMessage {
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 {
- if revert {
- // Remove the handlers.
- m.unwrap()
+ // Destroy the bottom leaf widgets first.
+ m.Avatar.Destroy()
+ m.HeaderLabel.Destroy()
- // Remove State's widgets from the containers.
- m.HeaderLabel.Destroy()
- m.MainBox.Remove(m.Content) // not ours, so don't destroy.
+ // Remove the content label from main then destroy it, in case destroying it
+ // ruins the label.
+ m.MainBox.Remove(m.Content)
+ m.MainBox.Destroy()
- // Remove the message from the grid.
- m.Avatar.Destroy()
- m.MainBox.Destroy()
- }
+ m.ClearBox()
- return m.State
+ return m.Unwrap()
}
+type full interface{ full() }
+
+func (m *FullMessage) full() {}
+
func formatLongTime(t time.Time) string {
- return `` + humanize.TimeAgoLong(t) + ``
+ return ` ` + humanize.TimeAgoLong(t) + ``
}
type FullSendingMessage struct {
diff --git a/internal/ui/messages/container/list.go b/internal/ui/messages/container/list.go
index 2517a67..5f6ec35 100644
--- a/internal/ui/messages/container/list.go
+++ b/internal/ui/messages/container/list.go
@@ -1,6 +1,7 @@
package container
import (
+ "log"
"strings"
"time"
@@ -21,6 +22,19 @@ type messageKey struct {
func nonceKey(nonce string) messageKey { return messageKey{nonce, true} }
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 {
name, err := n.GetName()
if err != nil {
@@ -38,7 +52,7 @@ func parseKeyFromNamer(n primitives.Namer) messageKey {
case "nonce":
return messageKey{id: parts[1], nonce: true}
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.
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,
@@ -149,32 +161,38 @@ func (c *ListStore) MessagesLen() int {
// TODO: combine compact and full so they share the same attach method.
func (c *ListStore) SwapMessage(msg MessageRow) bool {
// Unwrap msg from a *messageRow if it's not already.
- m, ok := msg.(*messageRow)
- if ok {
- msg = m.MessageRow
+ if mrow, ok := msg.(*messageRow); ok {
+ msg = mrow.MessageRow
}
- msgState := msg.Unwrap(false)
+ state := msg.Unwrap()
// Get the current message's index.
- oldMsg, ix := c.findIndex(msgState.ID)
+ oldMsg, ix := c.findIndex(state.ID)
if ix == -1 {
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.
- c.ListBox.Remove(oldState.Row)
+ // Remove the to-be-replaced message box.
+ // 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
// 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.
- row := c.messages[idKey(msgState.ID)]
- row.MessageRow = msg
- c.bindMessage(row)
+ c.messages[newKey(state)] = &row
+ c.bindMessage(&row)
return true
}
@@ -241,6 +259,15 @@ func (c *ListStore) findIndex(findID cchat.ID) (found *messageRow, index int) {
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) {
var r *messageRow
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 != nil {
- // Ignore sending messages.
+ // Ignore sending messages if presend is false.
if (presend || gridMsg.presend == nil) && fn(gridMsg) {
r = gridMsg
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
-// 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 {
+// true on that message.
+func (c *ListStore) FindMessage(isMessage func(MessageRow) bool) (MessageRow, int) {
+ msg, ix := c.findMessage(true, func(row *messageRow) bool {
return isMessage(row.MessageRow)
})
- return unwrapRow(msg)
+ return unwrapRow(msg), ix
}
func (c *ListStore) nthMessage(n int) *messageRow {
@@ -294,16 +321,6 @@ func (c *ListStore) NthMessage(n int) MessageRow {
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
// exists for backwards compatibility.
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) {
- state := msgc.Unwrap(false)
-
// Bind the message ID to the row so we can easily do a lookup.
key := messageKey{
- id: state.ID,
+ id: msgc.state.ID,
}
- if state.Nonce != "" {
- key.id = state.Nonce
+ if msgc.state.Nonce != "" {
+ key.id = msgc.state.Nonce
key.nonce = true
}
- state.Row.SetName(key.name())
+ msgc.state.Row.SetName(key.name())
msgc.MessageRow.SetReferenceHighlighter(c)
c.Controller.BindMenu(msgc.MessageRow)
}
-func (c *ListStore) AddMessage(msg MessageRow) {
- state := msg.Unwrap(false)
+func (c *ListStore) AddMessageAt(msg MessageRow, ix int) {
+ state := msg.Unwrap()
defer c.Controller.AuthorEvent(state.Author.ID)
@@ -373,37 +388,34 @@ func (c *ListStore) AddMessage(msg MessageRow) {
return
}
- // 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.
- before, index := c.findMessage(true, func(before *messageRow) bool {
- return before.Unwrap(false).Time.After(state.Time)
- })
+ // Attempt to guess if this is a presend message or not. This should be
+ // unwrapped once it's finalized.
+ presend, _ := msg.(message.Presender)
msgc := &messageRow{
MessageRow: msg,
+ presend: presend,
+ state: state,
}
// Add the message. If before is nil, then the to-be-inserted message is the
// earliest message, therefore we prepend it.
- if before == nil {
- index = 0
+ if ix < 0 {
+ ix = 0
c.ListBox.Prepend(state.Row)
} else {
- index++ // insert right after
+ ix++ // insert right after
// Fast path: Insert did appear a lot on profiles, so we can try and use
// Add over Insert when we know.
- if c.MessagesLen() == index {
+ if c.MessagesLen() == ix {
c.ListBox.Add(state.Row)
} else {
- c.ListBox.Insert(state.Row, index)
+ c.ListBox.Insert(state.Row, ix)
}
}
- // Set the ID into the message map.
- c.messages[idKey(state.ID)] = msgc
-
+ c.messages[newKey(state)] = 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.
primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) {
id := parseKeyFromNamer(v.(primitives.Namer))
- gridMsg := c.message(id.expand())
- state := gridMsg.Unwrap(false)
-
- if state.ID != "" {
- delete(c.messages, idKey(state.ID))
+ mr, ok := c.messages[id]
+ if !ok {
+ log.Panicln("message with ID", id, "not found in map")
}
- if state.Nonce != "" {
- delete(c.messages, nonceKey(state.Nonce))
- }
-
- destroyMsg(gridMsg)
+ delete(c.messages, id)
+ destroyMsg(mr)
n--
return n == 0
@@ -464,7 +471,7 @@ func (c *ListStore) HighlightReference(ref markup.ReferenceSegment) {
func (c *ListStore) Highlight(msg MessageRow) {
gts.ExecAsync(func() {
- state := msg.Unwrap(false)
+ state := msg.Unwrap()
state.Row.GrabFocus()
c.ListBox.DragHighlightRow(state.Row)
gts.DoAfter(2*time.Second, c.ListBox.DragUnhighlightRow)
@@ -472,7 +479,6 @@ func (c *ListStore) Highlight(msg MessageRow) {
}
func destroyMsg(row *messageRow) {
- state := row.Unwrap(true)
- state.Author.Name.Stop()
- state.Row.Destroy()
+ row.state.Author.Name.Stop()
+ row.state.Row.Destroy()
}
diff --git a/internal/ui/messages/input/keydown.go b/internal/ui/messages/input/keydown.go
index f423989..05630d3 100644
--- a/internal/ui/messages/input/keydown.go
+++ b/internal/ui/messages/input/keydown.go
@@ -54,7 +54,7 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
return false
}
- id := msgr.Unwrap(false).ID
+ id := msgr.Unwrap().ID
// If we don't support message editing, then passthrough events.
if !f.Editable(id) {
diff --git a/internal/ui/messages/input/username/username.go b/internal/ui/messages/input/username/username.go
index af3f239..f8cb536 100644
--- a/internal/ui/messages/input/username/username.go
+++ b/internal/ui/messages/input/username/username.go
@@ -103,19 +103,19 @@ func (u *Container) shouldReveal() bool {
func (u *Container) Reset() {
u.SetRevealChild(false)
+ u.State.ID = ""
u.State.Name.Stop()
}
// Update is not thread-safe.
func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) {
- // Set the fallback username.
- u.State.Name.BindNamer(u.main, "destroy", session)
- // Reveal the name if it's not empty.
+ u.State.ID = session.ID()
u.SetRevealChild(true)
- // Does messenger implement Nicknamer? If yes, use it.
if nicknamer := messenger.AsNicknamer(); nicknamer != nil {
u.State.Name.BindNamer(u.main, "destroy", nicknamer)
+ } else {
+ u.State.Name.BindNamer(u.main, "destroy", session)
}
}
diff --git a/internal/ui/messages/memberlist/memberlist.go b/internal/ui/messages/memberlist/memberlist.go
index ad3b778..6be0f2b 100644
--- a/internal/ui/messages/memberlist/memberlist.go
+++ b/internal/ui/messages/memberlist/memberlist.go
@@ -238,11 +238,12 @@ func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
section.Box.PackStart(section.Body, false, false, 0)
section.Box.Show()
- var members = map[string]*Member{}
+ members := map[string]*Member{}
// On row click, show the mention popup if any.
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.
for _, member := range members {
if member.ListBoxRow.GetIndex() == i {
@@ -253,6 +254,7 @@ func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
section.name.QueueNamer(context.Background(), sect)
section.Header.Connect("destroy", section.name.Stop)
+ section.Members = members
return section
}
@@ -328,12 +330,30 @@ var memberBoxCSS = primitives.PrepareClassCSS("member-box", `
}
`)
-var avatarMemberCSS = primitives.PrepareClassCSS("avatar-member", `
- .avatar-member {
- padding-right: 10px;
+var avatarBoxMemberCSS = primitives.PrepareClassCSS("avatar-box-member", `
+ .avatar-box-member {
+ 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 {
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.Show()
- m.Avatar = roundimage.NewStillImage(evb, 9999)
+ m.Avatar = roundimage.NewStillImage(evb, 0)
m.Avatar.SetSize(AvatarSize)
m.Avatar.SetPlaceholderIcon("user-info-symbolic", AvatarSize)
m.Avatar.Show()
- avatarMemberCSS(m.Avatar)
-
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.SetUseMarkup(true)
m.Name.SetXAlign(0)
m.Name.SetEllipsize(pango.ELLIPSIZE_END)
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 {
- out := markup.RenderCmplx(rich)
+ out := markup.RenderCmplxWithConfig(rich, markup.RenderConfig{
+ NoMentionLinks: true,
+ })
- if m.status != cchat.StatusUnknown {
- out.Markup = fmt.Sprintf(
- `● %s`,
- statusColors(member.Status()), out.Markup,
- )
+ if statusClass != "" {
+ styler.RemoveClass(statusClass)
}
+ statusClass = statusClassName(m.status)
+ styler.AddClass(statusClass)
+
if !m.second.IsEmpty() {
out.Markup += fmt.Sprintf(
- "\n"+`%s`,
+ ``+"\n"+`%s`,
markup.Render(m.second),
)
}
@@ -376,7 +407,7 @@ func NewMember(member cchat.ListMember) *Member {
})
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.Show()
memberBoxCSS(m.Main)
@@ -392,6 +423,25 @@ func NewMember(member cchat.ListMember) *Member {
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{
NoMentionLinks: true,
NoReferencing: true,
diff --git a/internal/ui/messages/message/message.go b/internal/ui/messages/message/message.go
index bc8344f..a18376c 100644
--- a/internal/ui/messages/message/message.go
+++ b/internal/ui/messages/message/message.go
@@ -19,10 +19,11 @@ import (
// made for containers to override; methods not meant to be override are not
// exposed and will be done directly on the State.
type Container interface {
- // Unwrap unwraps the message container and, if revert is true, revert the
- // state to a clean version. Containers must implement this method by
- // itself.
- Unwrap(revert bool) *State
+ // Unwrap returns the internal message state.
+ Unwrap() *State
+ // Revert unwraps and reverts all widget changes to the internal state then
+ // returns that state.
+ Revert() *State
// UpdateContent updates the underlying content widget.
UpdateContent(content text.Rich, edited bool)
@@ -50,8 +51,7 @@ type State struct {
MenuItems []menu.Item
}
-// NewState creates a new message state with the given ID and nonce. It does not
-// update the widgets, so FillContainer should be called afterwards.
+// NewState creates a new message state with the given MessageCreate.
func NewState(msg cchat.MessageCreate) *State {
author := msg.Author()
@@ -61,6 +61,7 @@ func NewState(msg cchat.MessageCreate) *State {
c.ID = msg.ID()
c.Time = msg.Time()
c.Nonce = msg.Nonce()
+ c.UpdateContent(msg.Content(), false)
return c
}
@@ -69,8 +70,7 @@ func NewState(msg cchat.MessageCreate) *State {
// immediately afterwards; it is invalid once the state is used.
func NewEmptyState() *State {
ctbody := labeluri.NewLabel(text.Rich{})
- ctbody.SetVExpand(true)
- ctbody.SetHAlign(gtk.ALIGN_START)
+ ctbody.SetHAlign(gtk.ALIGN_FILL)
ctbody.SetEllipsize(pango.ELLIPSIZE_NONE)
ctbody.SetLineWrap(true)
ctbody.SetLineWrapMode(pango.WRAP_WORD_CHAR)
@@ -84,10 +84,11 @@ func NewEmptyState() *State {
// Wrap the content label inside a content box.
ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
- ctbox.SetHExpand(true)
ctbox.PackStart(ctbody, false, false, 0)
+ ctbox.SetHAlign(gtk.ALIGN_FILL)
ctbox.Show()
+ // Box that belongs to the implementations of messages.
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.Show()
@@ -122,13 +123,38 @@ func NewEmptyState() *State {
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.
func (m *State) SetClass(class string) {
if m.class != "" {
primitives.RemoveClass(m.Row, m.class)
}
- primitives.AddClass(m.Row, class)
+ if class != "" {
+ primitives.AddClass(m.Row, class)
+ }
+
m.class = class
}
@@ -153,3 +179,6 @@ func (m *State) UpdateContent(content text.Rich, edited bool) {
func (m *State) Focusable() gtk.IWidget {
return m.Content
}
+
+// Unwrap returns itself.
+func (m *State) Unwrap() *State { return m }
diff --git a/internal/ui/messages/message/sending.go b/internal/ui/messages/message/sending.go
index b84bc42..b1885ec 100644
--- a/internal/ui/messages/message/sending.go
+++ b/internal/ui/messages/message/sending.go
@@ -45,14 +45,10 @@ type PresendState struct {
uploads *attachment.MessageUploader
}
-var (
- _ Presender = (*PresendState)(nil)
-)
+var _ Presender = (*PresendState)(nil)
-type SendMessageData struct {
-}
-
-// NewPresendState creates a new presend state.
+// NewPresendState creates a new presend state. The caller must call one of the
+// state setters, usually SetLoading.
func NewPresendState(self *Author, msg PresendMessage) *PresendState {
c := NewEmptyState()
c.Author = self
@@ -64,7 +60,7 @@ func NewPresendState(self *Author, msg PresendMessage) *PresendState {
presend: msg,
uploads: attachment.NewMessageUploader(msg.Files()),
}
- p.SetLoading()
+ // p.SetLoading()
return p
}
diff --git a/internal/ui/messages/msgctrl.go b/internal/ui/messages/msgctrl.go
index 686846f..4aecdd1 100644
--- a/internal/ui/messages/msgctrl.go
+++ b/internal/ui/messages/msgctrl.go
@@ -84,7 +84,7 @@ func (mc *MessageControl) Enable(msg container.MessageRow, names MessageItemName
mc.SetSensitive(true)
mc.SetRevealChild(true && !mc.hide)
- unwrap := msg.Unwrap(false)
+ unwrap := msg.Unwrap()
mc.Reply.bind(menu.FindItemFunc(unwrap.MenuItems, names.Reply))
mc.Edit.bind(menu.FindItemFunc(unwrap.MenuItems, names.Edit))
diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go
index b6b7875..bc3aea3 100644
--- a/internal/ui/messages/view.go
+++ b/internal/ui/messages/view.go
@@ -114,9 +114,10 @@ func NewView(c Controller) *View {
view.MsgBox.Show()
view.Scroller = autoscroll.NewScrolledWindow()
- view.Scroller.Add(view.MsgBox)
+ view.Scroller.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
view.Scroller.SetVExpand(true)
view.Scroller.SetHExpand(true)
+ view.Scroller.Add(view.MsgBox)
view.Scroller.Show()
messageScroller(view.Scroller)
@@ -352,12 +353,12 @@ func (v *View) JoinServer(ses *session.Row, srv *server.ServerRow, bc traverse.B
}
func (v *View) FetchBacklog() {
- var backlogger = v.state.Backlogger()
+ backlogger := v.state.Backlogger()
if backlogger == nil {
return
}
- var firstMsg = v.Container.FirstMessage()
+ firstMsg := container.FirstMessage(v.Container)
if firstMsg == nil {
return
}
@@ -365,12 +366,12 @@ func (v *View) FetchBacklog() {
// Set the window as busy. TODO: loading circles.
v.ctrl.OnMessageBusy()
- var done = func() {
+ done := func() {
v.ctrl.OnMessageDone()
v.Container.Highlight(firstMsg)
}
- firstID := firstMsg.Unwrap(false).ID
+ firstID := firstMsg.Unwrap().ID
gts.Async(func() (func(), error) {
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 msg.Unwrap(false).Author
+ return msg.Unwrap().Author
}
// Author returns the author from the message list with the given author ID.
func (v *View) Author(authorID cchat.ID) rich.LabelStateStorer {
- msg := v.Container.FindMessage(func(msg container.MessageRow) bool {
- return msg.Unwrap(false).Author.ID == authorID
+ msg, _ := v.Container.FindMessage(func(msg container.MessageRow) bool {
+ return msg.Unwrap().Author.ID == authorID
})
if msg == nil {
return nil
}
- state := msg.Unwrap(false)
+ state := msg.Unwrap()
return &state.Author.Name
}
// LatestMessageFrom returns the last message ID with that author.
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) {
state := message.NewPresendState(v.InputView.Username.State, msg)
msgr := v.Container.NewPresendMessage(state)
-
- v.retryMessage(msgr)
+ v.retryMessage(state, msgr)
}
// 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
if sender == nil {
return
}
+ // Ensure the message is set to loading.
+ presend.SetLoading()
+
go func() {
- if err := sender.Send(presend.SendingMessage()); err != nil {
- // Set the message's state to errored again, but we don't need to
- // rebind the menu.
- 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)
- }),
- }
- })
+ err := sender.Send(presend.SendingMessage())
+ if err == nil {
+ return
}
+
+ // 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
// states and callbacks.
func (v *View) BindMenu(msg container.MessageRow) {
- state := msg.Unwrap(false)
+ state := msg.Unwrap()
// Add 1 for the edit menu item.
var mitems = []menu.Item{
diff --git a/internal/ui/primitives/roundimage/roundimage.go b/internal/ui/primitives/roundimage/roundimage.go
index 9ab853f..a9e0953 100644
--- a/internal/ui/primitives/roundimage/roundimage.go
+++ b/internal/ui/primitives/roundimage/roundimage.go
@@ -173,17 +173,15 @@ func (i *Image) SetImageURLInto(url string, otherImage httputil.ImageContainer)
return
}
- if i.icon.name != "" {
- primitives.SetImageIcon(i, i.icon.name, i.icon.size)
- goto noImage
- }
-
if i.ifNone != nil {
i.ifNone(ctx)
return
}
-noImage:
+ if i.icon.name != "" {
+ primitives.SetImageIcon(i, i.icon.name, i.icon.size)
+ }
+
i.Image.SetFromPixbuf(nil)
i.cancel()
}
@@ -216,9 +214,14 @@ func (i *Image) drawer(image *gtk.Image, cc *cairo.Context) bool {
return false
}
- a := image.GetAllocation()
- w := float64(a.GetWidth())
- h := float64(a.GetHeight())
+ var w, h float64
+ if reqW, reqH := image.GetSizeRequest(); reqW > 0 && reqH > 0 {
+ w = float64(reqW)
+ h = float64(reqH)
+ } else {
+ w = float64(image.GetAllocatedWidth())
+ h = float64(image.GetAllocatedHeight())
+ }
min := w
// Use the largest side for radius calculation.
diff --git a/internal/ui/rich/rich.go b/internal/ui/rich/rich.go
index 6d69a64..f8e906f 100644
--- a/internal/ui/rich/rich.go
+++ b/internal/ui/rich/rich.go
@@ -62,29 +62,23 @@ func (namec *NameContainer) Stop() {
if namec.state != nil {
namec.state.Stop()
namec.LabelState.setLabel(text.Plain(""))
+ } else {
+ namec.state = &containerState{
+ current: func() {},
+ stop: func() {},
+ }
+ runtime.SetFinalizer(namec.state, (*containerState).Stop)
}
}
func (state *containerState) Stop() {
- if state.current != nil {
- state.current()
- state.current = nil
- }
-
- if state.stop != nil {
- state.stop()
- state.stop = nil
- }
+ state.current()
+ state.stop()
}
// QueueNamer tries using the namer in the background and queue the setter onto
// the next GLib loop iteration.
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()
ctx, cancel := context.WithCancel(ctx)
@@ -98,7 +92,6 @@ func (namec *NameContainer) QueueNamer(ctx context.Context, namer cchat.Namer) {
gts.ExecAsync(func() {
namec.state.current()
- namec.state.current = nil
namec.state.stop = stop
})
}()
@@ -115,11 +108,11 @@ func (namec *NameContainer) BindNamer(w primitives.Connector, sig string, namer
// namec.Stop()
// 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
// // might reference w indirectly.
- // w.Connect(sig, namec.Stop)
+ // handle := w.Connect(sig, namec.Stop)
// go func() {
// stop, err := namer.Name(ctx, namec)
@@ -128,9 +121,18 @@ func (namec *NameContainer) BindNamer(w primitives.Connector, sig string, namer
// }
// gts.ExecAsync(func() {
- // namec.current()
- // namec.current = nil
- // namec.stop = stop // nil is OK.
+ // namec.state.current()
+
+ // if stop != nil {
+ // namec.state.stop = func() {
+ // w.HandlerDisconnect(handle)
+ // stop()
+ // }
+ // } else {
+ // namec.state.stop = func() {
+ // w.HandlerDisconnect(handle)
+ // }
+ // }
// })
// }()
}
diff --git a/internal/ui/service/header.go b/internal/ui/service/header.go
index 7018db7..c33413c 100644
--- a/internal/ui/service/header.go
+++ b/internal/ui/service/header.go
@@ -124,17 +124,13 @@ func (h *Header) SetSessionMenu(s *session.Row) {
}
type sizeBinder interface {
- primitives.Connector
- GetAllocatedWidth() int
+ gtk.IWidget
}
var _ sizeBinder = (*List)(nil)
func (h *Header) AppMenuBindSize(c sizeBinder) {
- update := func(c sizeBinder) {
- h.AppMenu.SetSizeRequest(c.GetAllocatedWidth(), -1)
- }
-
- c.Connect("size-allocate", update)
- update(c)
+ sg, _ := gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL)
+ sg.AddWidget(c)
+ sg.AddWidget(h.AppMenu)
}
diff --git a/internal/ui/service/session/servers.go b/internal/ui/service/session/servers.go
index 7579378..d5a80b0 100644
--- a/internal/ui/service/session/servers.go
+++ b/internal/ui/service/session/servers.go
@@ -1,8 +1,6 @@
package session
import (
- "fmt"
-
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"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/traverse"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/serverpane"
+ "github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk"
- "github.com/gotk3/gotk3/pango"
)
const FaceSize = 48 // gtk.ICON_SIZE_DIALOG
@@ -212,33 +210,20 @@ func (s *Servers) setFailed(err error) {
s.Stack.Remove(w)
}
- // Create a BLANK label for padding.
- ltop, _ := gtk.LabelNew("")
- ltop.Show()
-
// 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.Connect("clicked", func(interface{}) { s.load() })
- // Create a bottom label for the error itself.
- lerr, _ := gtk.LabelNew("")
- lerr.SetSingleLineMode(true)
- lerr.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
- lerr.SetMarkup(fmt.Sprintf(
- `Error: %s`,
- humanize.Error(err),
- ))
- lerr.Show()
+ page := handy.StatusPageNew()
+ page.SetTitle("Error")
+ page.SetIconName("dialog-error")
+ page.SetTooltipText(err.Error())
+ page.SetDescription(humanize.Error(err))
+ page.Add(btn)
- // Add these items into the box.
- 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.AddNamed(page, "error")
s.Stack.SetVisibleChildName("error")
}
diff --git a/profile.go b/profile.go
deleted file mode 100644
index 1450c3c..0000000
--- a/profile.go
+++ /dev/null
@@ -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))
- }()
-}
diff --git a/shell.nix b/shell.nix
index 5cfc805..8201427 100644
--- a/shell.nix
+++ b/shell.nix
@@ -1,21 +1,19 @@
-{ pkgs ? import {} }:
+{ unstable ? import {} }:
-pkgs.stdenv.mkDerivation rec {
+unstable.stdenv.mkDerivation rec {
name = "cchat-gtk";
version = "0.0.2";
- buildInputs = [
- pkgs.libhandy
- pkgs.gnome3.gspell
- pkgs.gnome3.glib
- pkgs.gnome3.gtk
+ buildInputs = with unstable; [
+ libhandy
+ gnome3.gspell
+ gnome3.glib
+ gnome3.gtk
];
- nativeBuildInputs = with pkgs; [
- pkgconfig go
+ nativeBuildInputs = with unstable; [
+ pkgconfig
+ go
+ wrapGAppsHook
];
-
- # Debug flags.
- CGO_CFLAGS = "-g";
- CGO_CXXFLAGS = "-g";
}