Refactor server list, minor appearance tweaks
parent
e73f9a099b
commit
095107180d
4
go.mod
4
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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) })
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = `<span font_weight="600">` + output.Markup + "</span>"
|
||||
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 `<span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>`
|
||||
return ` <span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>`
|
||||
}
|
||||
|
||||
type FullSendingMessage struct {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
`<span color="#%06X" size="large">●</span> %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"+`<span alpha="85%%"><sup>%s</sup></span>`,
|
||||
`<span alpha="85%%" size="small">`+"\n"+`%s</span>`,
|
||||
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,
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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())
|
||||