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"; }