Refactor server list, minor appearance tweaks
This commit is contained in:
parent
e73f9a099b
commit
095107180d
4
go.mod
4
go.mod
|
@ -2,13 +2,11 @@ module github.com/diamondburned/cchat-gtk
|
||||||
|
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
replace github.com/diamondburned/cchat-discord => ../cchat-discord
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Xuanwo/go-locale v1.0.0
|
github.com/Xuanwo/go-locale v1.0.0
|
||||||
github.com/alecthomas/chroma v0.7.3
|
github.com/alecthomas/chroma v0.7.3
|
||||||
github.com/diamondburned/cchat v0.6.4
|
github.com/diamondburned/cchat v0.6.4
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff
|
github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799
|
||||||
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828
|
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828
|
||||||
github.com/diamondburned/handy v0.0.0-20210329054445-387ad28eb2c2
|
github.com/diamondburned/handy v0.0.0-20210329054445-387ad28eb2c2
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
|
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
|
||||||
|
|
2
go.sum
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-20210326063215-9eb392a95413/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff h1:p5XYPavnJ89wrJAf4ns6f1OfHQz5NMU9uXlX3EiKdfU=
|
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff h1:p5XYPavnJ89wrJAf4ns6f1OfHQz5NMU9uXlX3EiKdfU=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
|
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799 h1:xxqeuAx0T9SsS8DYKe4jxzL2saEpLyQeAttD0sX/g1E=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg=
|
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
|
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=
|
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=
|
||||||
|
|
|
@ -96,8 +96,6 @@ func Restore() {
|
||||||
log.Error(errors.Wrap(err, "Failed to unmarshal main config.json"))
|
log.Error(errors.Wrap(err, "Failed to unmarshal main config.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printlnf("To restore: %#v", toRestore)
|
|
||||||
|
|
||||||
for path, v := range toRestore {
|
for path, v := range toRestore {
|
||||||
if err := UnmarshalFromFile(path, v); err != nil {
|
if err := UnmarshalFromFile(path, v); err != nil {
|
||||||
log.Error(errors.Wrapf(err, "Failed to unmarshal %s", path))
|
log.Error(errors.Wrapf(err, "Failed to unmarshal %s", path))
|
||||||
|
|
|
@ -22,17 +22,23 @@ func NewContainer(ctrl container.Controller) *Container {
|
||||||
|
|
||||||
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
|
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
|
||||||
msg := WrapPresendMessage(state)
|
msg := WrapPresendMessage(state)
|
||||||
c.AddMessage(msg)
|
c.addMessage(msg)
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
msg := WrapMessage(message.NewState(msg))
|
msg := WrapMessage(message.NewState(msg))
|
||||||
c.ListContainer.AddMessage(msg)
|
c.addMessage(msg)
|
||||||
|
c.CleanMessages()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) addMessage(msg container.MessageRow) {
|
||||||
|
_, at := container.InsertPosition(c, msg.Unwrap().Time)
|
||||||
|
c.AddMessageAt(msg, at)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
|
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
|
||||||
gts.ExecAsync(func() { container.UpdateMessage(c, msg) })
|
gts.ExecAsync(func() { container.UpdateMessage(c, msg) })
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,13 +94,9 @@ func (m Message) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
|
||||||
m.Username.SetReferenceHighlighter(r)
|
m.Username.SetReferenceHighlighter(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Message) Unwrap(revert bool) *message.State {
|
func (m Message) Revert() *message.State {
|
||||||
if revert {
|
m.unwrap()
|
||||||
m.unwrap()
|
m.ClearBox()
|
||||||
|
|
||||||
primitives.RemoveChildren(m)
|
return m.Unwrap()
|
||||||
m.SetClass("")
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.State
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
@ -37,21 +39,21 @@ type Container interface {
|
||||||
// NewPresendMessage creates and adds a presend message state into the list.
|
// NewPresendMessage creates and adds a presend message state into the list.
|
||||||
NewPresendMessage(state *message.PresendState) PresendMessageRow
|
NewPresendMessage(state *message.PresendState) PresendMessageRow
|
||||||
|
|
||||||
// AddMessage adds a new message into the list.
|
// AddMessageAt adds a new message into the list at the given index.
|
||||||
AddMessage(row MessageRow)
|
AddMessageAt(row MessageRow, ix int)
|
||||||
|
|
||||||
// FirstMessage returns the first message in the buffer. Nil is returned if
|
// MessagesLen returns the current number of messages.
|
||||||
// there's nothing.
|
MessagesLen() int
|
||||||
FirstMessage() MessageRow
|
// NthMessage returns the nth message in the buffer or nil if there's
|
||||||
// LastMessage returns the last message in the buffer or nil if there's
|
|
||||||
// nothing.
|
// nothing.
|
||||||
LastMessage() MessageRow
|
NthMessage(ix int) MessageRow
|
||||||
|
|
||||||
// Message finds and returns the message, if any. It performs maximum 2
|
// Message finds and returns the message, if any. It performs maximum 2
|
||||||
// constant-time lookups.
|
// constant-time lookups.
|
||||||
Message(id cchat.ID, nonce string) MessageRow
|
Message(id cchat.ID, nonce string) MessageRow
|
||||||
// FindMessage finds a message that satisfies the given callback. It
|
// FindMessage finds a message that satisfies the given callback. It
|
||||||
// iterates the message buffer from latest to earliest.
|
// iterates the message buffer from latest to earliest.
|
||||||
FindMessage(isMessage func(MessageRow) bool) MessageRow
|
FindMessage(isMessage func(MessageRow) bool) (MessageRow, int)
|
||||||
|
|
||||||
// Highlight temporarily highlights the given message for a short while.
|
// Highlight temporarily highlights the given message for a short while.
|
||||||
Highlight(msg MessageRow)
|
Highlight(msg MessageRow)
|
||||||
|
@ -71,10 +73,56 @@ func UpdateMessage(ct Container, update cchat.MessageUpdate) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestMessageFrom returns the latest message from the given author ID.
|
// LatestMessageFrom returns the latest message from the given author ID.
|
||||||
func LatestMessageFrom(ct Container, authorID cchat.ID) MessageRow {
|
func LatestMessageFrom(ct Container, authorID cchat.ID) (MessageRow, int) {
|
||||||
return ct.FindMessage(func(msg MessageRow) bool {
|
finder, ok := ct.(messageFinder)
|
||||||
return msg.Unwrap(false).Author.ID == authorID
|
if !ok {
|
||||||
|
return ct.FindMessage(func(msg MessageRow) bool {
|
||||||
|
return msg.Unwrap().Author.ID == authorID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, ix := finder.findMessage(true, func(msg *messageRow) bool {
|
||||||
|
return msg.state.Author.ID == authorID
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return unwrapRow(msg), ix
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstMessage returns the first message in the buffer. Nil is returned if
|
||||||
|
// there's nothing.
|
||||||
|
func FirstMessage(ct Container) MessageRow {
|
||||||
|
return ct.NthMessage(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastMessage returns the last message in the buffer or nil if there's nothing.
|
||||||
|
func LastMessage(ct Container) MessageRow {
|
||||||
|
return ct.NthMessage(ct.MessagesLen() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPosition returns the message that is before the given time (or nil) and
|
||||||
|
// the new index of the message with the given timestamp. If -1 is returned,
|
||||||
|
// then there is no message prior, and the message should be prepended on top.
|
||||||
|
func InsertPosition(ct Container, t time.Time) (MessageRow, int) {
|
||||||
|
var row MessageRow
|
||||||
|
var mIx int
|
||||||
|
|
||||||
|
finder, ok := ct.(messageFinder)
|
||||||
|
if !ok {
|
||||||
|
row, mIx = ct.FindMessage(func(msg MessageRow) bool {
|
||||||
|
return t.After(msg.Unwrap().Time)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Iterate and compare timestamp to find where to insert a message. Note
|
||||||
|
// that "before" is the message that will go before the to-be-inserted
|
||||||
|
// method.
|
||||||
|
msg, ix := finder.findMessage(true, func(msg *messageRow) bool {
|
||||||
|
return t.After(msg.state.Time)
|
||||||
|
})
|
||||||
|
row = unwrapRow(msg)
|
||||||
|
mIx = ix
|
||||||
|
}
|
||||||
|
|
||||||
|
return row, mIx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controller is for menu actions.
|
// Controller is for menu actions.
|
||||||
|
@ -109,6 +157,7 @@ type ListContainer struct {
|
||||||
// messageRow w/ required internals
|
// messageRow w/ required internals
|
||||||
type messageRow struct {
|
type messageRow struct {
|
||||||
MessageRow
|
MessageRow
|
||||||
|
state *message.State
|
||||||
presend message.Presender // this shouldn't be here but i'm lazy
|
presend message.Presender // this shouldn't be here but i'm lazy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,11 +189,6 @@ func NewListContainer(ctrl Controller) *ListContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListContainer) AddMessage(row MessageRow) {
|
|
||||||
c.ListStore.AddMessage(row)
|
|
||||||
c.CleanMessages()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanMessages cleans up the oldest messages if the user is scrolled to the
|
// CleanMessages cleans up the oldest messages if the user is scrolled to the
|
||||||
// bottom. True is returned if there were changes.
|
// bottom. True is returned if there were changes.
|
||||||
func (c *ListContainer) CleanMessages() bool {
|
func (c *ListContainer) CleanMessages() bool {
|
||||||
|
|
|
@ -14,7 +14,7 @@ type Avatar struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAvatar(parent primitives.Connector) *Avatar {
|
func NewAvatar(parent primitives.Connector) *Avatar {
|
||||||
img := roundimage.NewStillImage(nil, 0)
|
img := roundimage.NewStillImage(parent, 0)
|
||||||
img.SetSizeRequest(AvatarSize, AvatarSize)
|
img.SetSizeRequest(AvatarSize, AvatarSize)
|
||||||
img.Show()
|
img.Show()
|
||||||
|
|
||||||
|
|
|
@ -10,20 +10,6 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collapsible is an interface for cozy messages to return whether or not
|
|
||||||
// they're full or collapsed.
|
|
||||||
type Collapsible interface {
|
|
||||||
// Compact returns true if the message is a compact one and not full.
|
|
||||||
Collapsed() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ Collapsible = (*CollapsedMessage)(nil)
|
|
||||||
_ Collapsible = (*CollapsedSendingMessage)(nil)
|
|
||||||
_ Collapsible = (*FullMessage)(nil)
|
|
||||||
_ Collapsible = (*FullSendingMessage)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AvatarSize = 40
|
AvatarSize = 40
|
||||||
AvatarMargin = 10
|
AvatarMargin = 10
|
||||||
|
@ -61,69 +47,58 @@ func NewContainer(ctrl container.Controller) *Container {
|
||||||
return &Container{ListContainer: c}
|
return &Container{ListContainer: c}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) findAuthorID(authorID string) container.MessageRow {
|
|
||||||
// Search the old author if we have any.
|
|
||||||
return c.ListStore.FindMessage(func(msgc container.MessageRow) bool {
|
|
||||||
return msgc.Unwrap(false).Author.ID == authorID
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitDuration = 10 * time.Minute
|
const splitDuration = 10 * time.Minute
|
||||||
|
|
||||||
// isCollapsible returns true if the given lastMsg has matching conditions with
|
// isCollapsible returns true if the given lastMsg has matching conditions with
|
||||||
// the given msg.
|
// the given msg.
|
||||||
func isCollapsible(last container.MessageRow, msg *message.State) bool {
|
func isCollapsible(last container.MessageRow, msg *message.State) bool {
|
||||||
if last == nil || msg.ID == "" {
|
if last == nil || msg == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMsg := last.Unwrap(false)
|
lastMsg := last.Unwrap()
|
||||||
|
|
||||||
return true &&
|
return true &&
|
||||||
lastMsg.Author.ID == msg.ID &&
|
lastMsg.Author.ID == msg.Author.ID &&
|
||||||
lastMsg.Time.Add(splitDuration).After(msg.Time)
|
lastMsg.Time.Add(splitDuration).After(msg.Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
|
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
|
||||||
msgr := NewPresendMessage(state, c.LastMessage())
|
before, at := container.InsertPosition(c, state.Time)
|
||||||
c.AddMessage(msgr)
|
msgr := NewPresendMessage(state, before)
|
||||||
|
c.AddMessageAt(msgr, at)
|
||||||
return msgr
|
return msgr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
|
before, at := container.InsertPosition(c, msg.Time())
|
||||||
state := message.NewState(msg)
|
state := message.NewState(msg)
|
||||||
msgr := NewMessage(state, c.LastMessage())
|
msgr := NewMessage(state, before)
|
||||||
|
c.AddMessageAt(msgr, at)
|
||||||
c.AddMessage(msgr)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMessage adds the given message.
|
// AddMessage adds the given message.
|
||||||
func (c *Container) AddMessage(msgr container.MessageRow) {
|
func (c *Container) AddMessageAt(msgr container.MessageRow, ix int) {
|
||||||
// Create the message in the parent's handler. This handler will also
|
// Create the message in the parent's handler. This handler will also
|
||||||
// wipe old messages.
|
// wipe old messages.
|
||||||
c.ListContainer.AddMessage(msgr)
|
c.ListContainer.AddMessageAt(msgr, ix)
|
||||||
|
|
||||||
// Did the handler wipe old messages? It will only do so if the user is
|
// Did the handler wipe old messages? It will only do so if the user is
|
||||||
// scrolled to the bottom.
|
// scrolled to the bottom.
|
||||||
if c.ListContainer.CleanMessages() {
|
if c.ListContainer.CleanMessages() {
|
||||||
// We need to uncollapse the first (top) message. No length check is
|
// We need to uncollapse the first (top) message. No length check is
|
||||||
// needed here, as we just inserted a message.
|
// needed here, as we just inserted a message.
|
||||||
c.uncompact(c.FirstMessage())
|
c.uncompact(container.FirstMessage(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've prepended the message, then see if we need to collapse the
|
// If we've prepended the message, then see if we need to collapse the
|
||||||
// second message.
|
// second message.
|
||||||
if first := c.ListContainer.FirstMessage(); first != nil {
|
if ix == -1 {
|
||||||
firstState := first.Unwrap(false)
|
// If the author is the same, then collapse.
|
||||||
msgState := msgr.Unwrap(false)
|
if sec := c.NthMessage(1); isCollapsible(sec, msgr.Unwrap()) {
|
||||||
|
c.compact(sec)
|
||||||
if firstState.ID == msgState.ID {
|
|
||||||
// If the author is the same, then collapse.
|
|
||||||
if sec := c.NthMessage(1); isCollapsible(sec, firstState) {
|
|
||||||
c.compact(sec)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,10 +127,10 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msgHeader := msg.Unwrap(false)
|
msgHeader := msg.Unwrap()
|
||||||
|
|
||||||
prevHeader := prev.Unwrap(false)
|
prevHeader := prev.Unwrap()
|
||||||
nextHeader := next.Unwrap(false)
|
nextHeader := next.Unwrap()
|
||||||
|
|
||||||
// Check if the last message is the author's (relative to i):
|
// Check if the last message is the author's (relative to i):
|
||||||
if prevHeader.Author.ID == msgHeader.Author.ID {
|
if prevHeader.Author.ID == msgHeader.Author.ID {
|
||||||
|
@ -176,11 +151,21 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) uncompact(msg container.MessageRow) {
|
func (c *Container) uncompact(msg container.MessageRow) {
|
||||||
full := WrapFullMessage(msg.Unwrap(true))
|
_, isFull := msg.(full)
|
||||||
|
if isFull {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
full := WrapFullMessage(msg.Revert())
|
||||||
c.ListStore.SwapMessage(full)
|
c.ListStore.SwapMessage(full)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) compact(msg container.MessageRow) {
|
func (c *Container) compact(msg container.MessageRow) {
|
||||||
compact := WrapCollapsedMessage(msg.Unwrap(true))
|
_, isCollapsed := msg.(collapsed)
|
||||||
|
if isCollapsed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
compact := WrapCollapsedMessage(msg.Revert())
|
||||||
c.ListStore.SwapMessage(compact)
|
c.ListStore.SwapMessage(compact)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ func WrapCollapsedMessage(gc *message.State) *CollapsedMessage {
|
||||||
ts.SetMarginStart(container.ColumnSpacing * 2)
|
ts.SetMarginStart(container.ColumnSpacing * 2)
|
||||||
|
|
||||||
// Set Content's padding accordingly to FullMessage's main box.
|
// Set Content's padding accordingly to FullMessage's main box.
|
||||||
gc.Content.ToWidget().SetMarginEnd(container.ColumnSpacing * 2)
|
gc.Content.SetMarginEnd(container.ColumnSpacing * 2)
|
||||||
|
|
||||||
gc.PackStart(ts, false, false, 0)
|
gc.PackStart(ts, false, false, 0)
|
||||||
gc.PackStart(gc.Content, true, true, 0)
|
gc.PackStart(gc.Content, true, true, 0)
|
||||||
|
@ -37,18 +37,19 @@ func WrapCollapsedMessage(gc *message.State) *CollapsedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CollapsedMessage) Collapsed() bool { return true }
|
func (c *CollapsedMessage) Revert() *message.State {
|
||||||
|
c.ClearBox()
|
||||||
func (c *CollapsedMessage) Unwrap(revert bool) *message.State {
|
c.Content.SetMarginEnd(0)
|
||||||
if revert {
|
c.Timestamp.Destroy()
|
||||||
// Remove State's widgets from the containers.
|
return c.Unwrap()
|
||||||
c.Remove(c.Timestamp)
|
|
||||||
c.Remove(c.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.State
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type collapsed interface {
|
||||||
|
collapsed()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CollapsedMessage) collapsed() {}
|
||||||
|
|
||||||
type CollapsedSendingMessage struct {
|
type CollapsedSendingMessage struct {
|
||||||
*CollapsedMessage
|
*CollapsedMessage
|
||||||
message.Presender
|
message.Presender
|
||||||
|
|
|
@ -14,9 +14,6 @@ import (
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TopFullMargin is the margin on top of every full message.
|
|
||||||
const TopFullMargin = 4
|
|
||||||
|
|
||||||
type FullMessage struct {
|
type FullMessage struct {
|
||||||
*message.State
|
*message.State
|
||||||
|
|
||||||
|
@ -37,12 +34,22 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", `
|
var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", `
|
||||||
|
.cozy-avatar {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Slightly dip down on click */
|
/* Slightly dip down on click */
|
||||||
.cozy-avatar:active {
|
.cozy-avatar:active {
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
var mainCSS = primitives.PrepareClassCSS("cozy-main", `
|
||||||
|
.cozy-main {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
|
func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
|
||||||
return WrapFullMessage(message.NewState(msg))
|
return WrapFullMessage(message.NewState(msg))
|
||||||
}
|
}
|
||||||
|
@ -54,7 +61,6 @@ func WrapFullMessage(gc *message.State) *FullMessage {
|
||||||
header.Show()
|
header.Show()
|
||||||
|
|
||||||
avatar := NewAvatar(gc.Row)
|
avatar := NewAvatar(gc.Row)
|
||||||
avatar.SetMarginTop(TopFullMargin / 2)
|
|
||||||
avatar.SetMarginStart(container.ColumnSpacing * 2)
|
avatar.SetMarginStart(container.ColumnSpacing * 2)
|
||||||
avatar.Connect("clicked", func(w gtk.IWidget) {
|
avatar.Connect("clicked", func(w gtk.IWidget) {
|
||||||
if output := header.Output(); len(output.Mentions) > 0 {
|
if output := header.Output(); len(output.Mentions) > 0 {
|
||||||
|
@ -72,7 +78,6 @@ func WrapFullMessage(gc *message.State) *FullMessage {
|
||||||
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
main.PackStart(header, false, false, 0)
|
main.PackStart(header, false, false, 0)
|
||||||
main.PackStart(gc.Content, false, false, 0)
|
main.PackStart(gc.Content, false, false, 0)
|
||||||
main.SetMarginTop(TopFullMargin)
|
|
||||||
main.SetMarginEnd(container.ColumnSpacing * 2)
|
main.SetMarginEnd(container.ColumnSpacing * 2)
|
||||||
main.SetMarginStart(container.ColumnSpacing)
|
main.SetMarginStart(container.ColumnSpacing)
|
||||||
main.Show()
|
main.Show()
|
||||||
|
@ -84,6 +89,11 @@ func WrapFullMessage(gc *message.State) *FullMessage {
|
||||||
gc.PackStart(main, true, true, 0)
|
gc.PackStart(main, true, true, 0)
|
||||||
gc.SetClass("cozy-full")
|
gc.SetClass("cozy-full")
|
||||||
|
|
||||||
|
removeUpdate := gc.Author.Name.OnUpdate(func() {
|
||||||
|
avatar.SetImage(gc.Author.Name.Image())
|
||||||
|
header.SetLabel(gc.Author.Name.Label())
|
||||||
|
})
|
||||||
|
|
||||||
msg := &FullMessage{
|
msg := &FullMessage{
|
||||||
State: gc,
|
State: gc,
|
||||||
timestamp: formatLongTime(gc.Time),
|
timestamp: formatLongTime(gc.Time),
|
||||||
|
@ -92,17 +102,14 @@ func WrapFullMessage(gc *message.State) *FullMessage {
|
||||||
MainBox: main,
|
MainBox: main,
|
||||||
HeaderLabel: header,
|
HeaderLabel: header,
|
||||||
|
|
||||||
unwrap: gc.Author.Name.OnUpdate(func() {
|
unwrap: func() { removeUpdate() },
|
||||||
avatar.SetImage(gc.Author.Name.Image())
|
|
||||||
header.SetLabel(gc.Author.Name.Label())
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header.SetRenderer(func(rich text.Rich) markup.RenderOutput {
|
cfg := markup.RenderConfig{}
|
||||||
cfg := markup.RenderConfig{}
|
cfg.NoReferencing = true
|
||||||
cfg.NoReferencing = true
|
cfg.SetForegroundAnchor(gc.ContentBodyStyle)
|
||||||
cfg.SetForegroundAnchor(gc.ContentBodyStyle)
|
|
||||||
|
|
||||||
|
header.SetRenderer(func(rich text.Rich) markup.RenderOutput {
|
||||||
output := markup.RenderCmplxWithConfig(rich, cfg)
|
output := markup.RenderCmplxWithConfig(rich, cfg)
|
||||||
output.Markup = `<span font_weight="600">` + output.Markup + "</span>"
|
output.Markup = `<span font_weight="600">` + output.Markup + "</span>"
|
||||||
output.Markup += msg.timestamp
|
output.Markup += msg.timestamp
|
||||||
|
@ -113,27 +120,30 @@ func WrapFullMessage(gc *message.State) *FullMessage {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FullMessage) Collapsed() bool { return false }
|
func (m *FullMessage) Revert() *message.State {
|
||||||
|
// Remove the handlers.
|
||||||
|
m.unwrap()
|
||||||
|
|
||||||
func (m *FullMessage) Unwrap(revert bool) *message.State {
|
// Destroy the bottom leaf widgets first.
|
||||||
if revert {
|
m.Avatar.Destroy()
|
||||||
// Remove the handlers.
|
m.HeaderLabel.Destroy()
|
||||||
m.unwrap()
|
|
||||||
|
|
||||||
// Remove State's widgets from the containers.
|
// Remove the content label from main then destroy it, in case destroying it
|
||||||
m.HeaderLabel.Destroy()
|
// ruins the label.
|
||||||
m.MainBox.Remove(m.Content) // not ours, so don't destroy.
|
m.MainBox.Remove(m.Content)
|
||||||
|
m.MainBox.Destroy()
|
||||||
|
|
||||||
// Remove the message from the grid.
|
m.ClearBox()
|
||||||
m.Avatar.Destroy()
|
|
||||||
m.MainBox.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.State
|
return m.Unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type full interface{ full() }
|
||||||
|
|
||||||
|
func (m *FullMessage) full() {}
|
||||||
|
|
||||||
func formatLongTime(t time.Time) string {
|
func formatLongTime(t time.Time) string {
|
||||||
return `<span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>`
|
return ` <span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullSendingMessage struct {
|
type FullSendingMessage struct {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -21,6 +22,19 @@ type messageKey struct {
|
||||||
func nonceKey(nonce string) messageKey { return messageKey{nonce, true} }
|
func nonceKey(nonce string) messageKey { return messageKey{nonce, true} }
|
||||||
func idKey(id cchat.ID) messageKey { return messageKey{id, false} }
|
func idKey(id cchat.ID) messageKey { return messageKey{id, false} }
|
||||||
|
|
||||||
|
// newKey creates a new message key.
|
||||||
|
func newKey(state *message.State) messageKey {
|
||||||
|
if state.ID != "" {
|
||||||
|
return messageKey{state.ID, false}
|
||||||
|
}
|
||||||
|
if state.Nonce != "" {
|
||||||
|
return messageKey{state.Nonce, true}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("state is missing both ID and Nonce: \n%#v\n", state)
|
||||||
|
return messageKey{}
|
||||||
|
}
|
||||||
|
|
||||||
func parseKeyFromNamer(n primitives.Namer) messageKey {
|
func parseKeyFromNamer(n primitives.Namer) messageKey {
|
||||||
name, err := n.GetName()
|
name, err := n.GetName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -38,7 +52,7 @@ func parseKeyFromNamer(n primitives.Namer) messageKey {
|
||||||
case "nonce":
|
case "nonce":
|
||||||
return messageKey{id: parts[1], nonce: true}
|
return messageKey{id: parts[1], nonce: true}
|
||||||
default:
|
default:
|
||||||
panic("Unknown prefix in row name " + parts[0])
|
panic("unknown prefix in message row name " + parts[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,9 +137,7 @@ func (c *ListStore) Reset() {
|
||||||
// Delegate removing children to the constructor.
|
// Delegate removing children to the constructor.
|
||||||
c.messages = make(map[messageKey]*messageRow, BacklogLimit+1)
|
c.messages = make(map[messageKey]*messageRow, BacklogLimit+1)
|
||||||
|
|
||||||
if c.self.ID != "" {
|
c.self.Name.Stop()
|
||||||
c.self.Name.Stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSelf sets the current author to presend. If ID is empty or Namer is nil,
|
// SetSelf sets the current author to presend. If ID is empty or Namer is nil,
|
||||||
|
@ -149,32 +161,38 @@ func (c *ListStore) MessagesLen() int {
|
||||||
// TODO: combine compact and full so they share the same attach method.
|
// TODO: combine compact and full so they share the same attach method.
|
||||||
func (c *ListStore) SwapMessage(msg MessageRow) bool {
|
func (c *ListStore) SwapMessage(msg MessageRow) bool {
|
||||||
// Unwrap msg from a *messageRow if it's not already.
|
// Unwrap msg from a *messageRow if it's not already.
|
||||||
m, ok := msg.(*messageRow)
|
if mrow, ok := msg.(*messageRow); ok {
|
||||||
if ok {
|
msg = mrow.MessageRow
|
||||||
msg = m.MessageRow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msgState := msg.Unwrap(false)
|
state := msg.Unwrap()
|
||||||
|
|
||||||
// Get the current message's index.
|
// Get the current message's index.
|
||||||
oldMsg, ix := c.findIndex(msgState.ID)
|
oldMsg, ix := c.findIndex(state.ID)
|
||||||
if ix == -1 {
|
if ix == -1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
oldState := oldMsg.Unwrap(false)
|
// Remove the previous message off the message map using the key from its
|
||||||
|
// state.
|
||||||
|
delete(c.messages, newKey(oldMsg.state))
|
||||||
|
|
||||||
// Remove the to-be-replaced message box. We should probably reuse the row.
|
// Remove the to-be-replaced message box.
|
||||||
c.ListBox.Remove(oldState.Row)
|
// TODO: We should probably reuse the row.
|
||||||
|
c.ListBox.Remove(oldMsg.state.Row)
|
||||||
|
|
||||||
// Add a row at index. The actual row we want to delete will be shifted
|
// Add a row at index. The actual row we want to delete will be shifted
|
||||||
// downwards.
|
// downwards.
|
||||||
c.ListBox.Insert(msgState.Row, ix)
|
c.ListBox.Insert(state.Row, ix)
|
||||||
|
|
||||||
|
row := messageRow{
|
||||||
|
MessageRow: msg,
|
||||||
|
state: state,
|
||||||
|
}
|
||||||
|
|
||||||
// Set the message into the map.
|
// Set the message into the map.
|
||||||
row := c.messages[idKey(msgState.ID)]
|
c.messages[newKey(state)] = &row
|
||||||
row.MessageRow = msg
|
c.bindMessage(&row)
|
||||||
c.bindMessage(row)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -241,6 +259,15 @@ func (c *ListStore) findIndex(findID cchat.ID) (found *messageRow, index int) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fast path interface.
|
||||||
|
type messageFinder interface {
|
||||||
|
findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ messageFinder = (*ListStore)(nil)
|
||||||
|
|
||||||
|
// findMessage finds a message with the given callback as the filter. If presend
|
||||||
|
// is false, then presend messages are ignored.
|
||||||
func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int) {
|
func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int) {
|
||||||
var r *messageRow
|
var r *messageRow
|
||||||
var i = c.MessagesLen() - 1
|
var i = c.MessagesLen() - 1
|
||||||
|
@ -251,7 +278,7 @@ func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messa
|
||||||
|
|
||||||
// If gridMsg is actually nil, then we have bigger issues.
|
// If gridMsg is actually nil, then we have bigger issues.
|
||||||
if gridMsg != nil {
|
if gridMsg != nil {
|
||||||
// Ignore sending messages.
|
// Ignore sending messages if presend is false.
|
||||||
if (presend || gridMsg.presend == nil) && fn(gridMsg) {
|
if (presend || gridMsg.presend == nil) && fn(gridMsg) {
|
||||||
r = gridMsg
|
r = gridMsg
|
||||||
return true
|
return true
|
||||||
|
@ -271,12 +298,12 @@ func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messa
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindMessage iterates backwards and returns the message if isMessage() returns
|
// FindMessage iterates backwards and returns the message if isMessage() returns
|
||||||
// true on that message. It does not search presend messages.
|
// true on that message.
|
||||||
func (c *ListStore) FindMessage(isMessage func(MessageRow) bool) MessageRow {
|
func (c *ListStore) FindMessage(isMessage func(MessageRow) bool) (MessageRow, int) {
|
||||||
msg, _ := c.findMessage(false, func(row *messageRow) bool {
|
msg, ix := c.findMessage(true, func(row *messageRow) bool {
|
||||||
return isMessage(row.MessageRow)
|
return isMessage(row.MessageRow)
|
||||||
})
|
})
|
||||||
return unwrapRow(msg)
|
return unwrapRow(msg), ix
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) nthMessage(n int) *messageRow {
|
func (c *ListStore) nthMessage(n int) *messageRow {
|
||||||
|
@ -294,16 +321,6 @@ func (c *ListStore) NthMessage(n int) MessageRow {
|
||||||
return unwrapRow(c.nthMessage(n))
|
return unwrapRow(c.nthMessage(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
// FirstMessage returns the first message.
|
|
||||||
func (c *ListStore) FirstMessage() MessageRow {
|
|
||||||
return c.NthMessage(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LastMessage returns the latest message.
|
|
||||||
func (c *ListStore) LastMessage() MessageRow {
|
|
||||||
return c.NthMessage(c.MessagesLen() - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message finds the message state in the container. It is not thread-safe. This
|
// Message finds the message state in the container. It is not thread-safe. This
|
||||||
// exists for backwards compatibility.
|
// exists for backwards compatibility.
|
||||||
func (c *ListStore) Message(msgID cchat.ID, nonce string) MessageRow {
|
func (c *ListStore) Message(msgID cchat.ID, nonce string) MessageRow {
|
||||||
|
@ -343,26 +360,24 @@ func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) bindMessage(msgc *messageRow) {
|
func (c *ListStore) bindMessage(msgc *messageRow) {
|
||||||
state := msgc.Unwrap(false)
|
|
||||||
|
|
||||||
// Bind the message ID to the row so we can easily do a lookup.
|
// Bind the message ID to the row so we can easily do a lookup.
|
||||||
key := messageKey{
|
key := messageKey{
|
||||||
id: state.ID,
|
id: msgc.state.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.Nonce != "" {
|
if msgc.state.Nonce != "" {
|
||||||
key.id = state.Nonce
|
key.id = msgc.state.Nonce
|
||||||
key.nonce = true
|
key.nonce = true
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Row.SetName(key.name())
|
msgc.state.Row.SetName(key.name())
|
||||||
msgc.MessageRow.SetReferenceHighlighter(c)
|
msgc.MessageRow.SetReferenceHighlighter(c)
|
||||||
|
|
||||||
c.Controller.BindMenu(msgc.MessageRow)
|
c.Controller.BindMenu(msgc.MessageRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListStore) AddMessage(msg MessageRow) {
|
func (c *ListStore) AddMessageAt(msg MessageRow, ix int) {
|
||||||
state := msg.Unwrap(false)
|
state := msg.Unwrap()
|
||||||
|
|
||||||
defer c.Controller.AuthorEvent(state.Author.ID)
|
defer c.Controller.AuthorEvent(state.Author.ID)
|
||||||
|
|
||||||
|
@ -373,37 +388,34 @@ func (c *ListStore) AddMessage(msg MessageRow) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate and compare timestamp to find where to insert a message. Note
|
// Attempt to guess if this is a presend message or not. This should be
|
||||||
// that "before" is the message that will go before the to-be-inserted
|
// unwrapped once it's finalized.
|
||||||
// method.
|
presend, _ := msg.(message.Presender)
|
||||||
before, index := c.findMessage(true, func(before *messageRow) bool {
|
|
||||||
return before.Unwrap(false).Time.After(state.Time)
|
|
||||||
})
|
|
||||||
|
|
||||||
msgc := &messageRow{
|
msgc := &messageRow{
|
||||||
MessageRow: msg,
|
MessageRow: msg,
|
||||||
|
presend: presend,
|
||||||
|
state: state,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the message. If before is nil, then the to-be-inserted message is the
|
// Add the message. If before is nil, then the to-be-inserted message is the
|
||||||
// earliest message, therefore we prepend it.
|
// earliest message, therefore we prepend it.
|
||||||
if before == nil {
|
if ix < 0 {
|
||||||
index = 0
|
ix = 0
|
||||||
c.ListBox.Prepend(state.Row)
|
c.ListBox.Prepend(state.Row)
|
||||||
} else {
|
} else {
|
||||||
index++ // insert right after
|
ix++ // insert right after
|
||||||
|
|
||||||
// Fast path: Insert did appear a lot on profiles, so we can try and use
|
// Fast path: Insert did appear a lot on profiles, so we can try and use
|
||||||
// Add over Insert when we know.
|
// Add over Insert when we know.
|
||||||
if c.MessagesLen() == index {
|
if c.MessagesLen() == ix {
|
||||||
c.ListBox.Add(state.Row)
|
c.ListBox.Add(state.Row)
|
||||||
} else {
|
} else {
|
||||||
c.ListBox.Insert(state.Row, index)
|
c.ListBox.Insert(state.Row, ix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the ID into the message map.
|
c.messages[newKey(state)] = msgc
|
||||||
c.messages[idKey(state.ID)] = msgc
|
|
||||||
|
|
||||||
c.bindMessage(msgc)
|
c.bindMessage(msgc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,19 +448,14 @@ func (c *ListStore) DeleteEarliest(n int) {
|
||||||
// after deleting, so we have to call Next manually before Removing.
|
// after deleting, so we have to call Next manually before Removing.
|
||||||
primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) {
|
primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) {
|
||||||
id := parseKeyFromNamer(v.(primitives.Namer))
|
id := parseKeyFromNamer(v.(primitives.Namer))
|
||||||
gridMsg := c.message(id.expand())
|
|
||||||
|
|
||||||
state := gridMsg.Unwrap(false)
|
mr, ok := c.messages[id]
|
||||||
|
if !ok {
|
||||||
if state.ID != "" {
|
log.Panicln("message with ID", id, "not found in map")
|
||||||
delete(c.messages, idKey(state.ID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.Nonce != "" {
|
delete(c.messages, id)
|
||||||
delete(c.messages, nonceKey(state.Nonce))
|
destroyMsg(mr)
|
||||||
}
|
|
||||||
|
|
||||||
destroyMsg(gridMsg)
|
|
||||||
|
|
||||||
n--
|
n--
|
||||||
return n == 0
|
return n == 0
|
||||||
|
@ -464,7 +471,7 @@ func (c *ListStore) HighlightReference(ref markup.ReferenceSegment) {
|
||||||
|
|
||||||
func (c *ListStore) Highlight(msg MessageRow) {
|
func (c *ListStore) Highlight(msg MessageRow) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
state := msg.Unwrap(false)
|
state := msg.Unwrap()
|
||||||
state.Row.GrabFocus()
|
state.Row.GrabFocus()
|
||||||
c.ListBox.DragHighlightRow(state.Row)
|
c.ListBox.DragHighlightRow(state.Row)
|
||||||
gts.DoAfter(2*time.Second, c.ListBox.DragUnhighlightRow)
|
gts.DoAfter(2*time.Second, c.ListBox.DragUnhighlightRow)
|
||||||
|
@ -472,7 +479,6 @@ func (c *ListStore) Highlight(msg MessageRow) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func destroyMsg(row *messageRow) {
|
func destroyMsg(row *messageRow) {
|
||||||
state := row.Unwrap(true)
|
row.state.Author.Name.Stop()
|
||||||
state.Author.Name.Stop()
|
row.state.Row.Destroy()
|
||||||
state.Row.Destroy()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
id := msgr.Unwrap(false).ID
|
id := msgr.Unwrap().ID
|
||||||
|
|
||||||
// If we don't support message editing, then passthrough events.
|
// If we don't support message editing, then passthrough events.
|
||||||
if !f.Editable(id) {
|
if !f.Editable(id) {
|
||||||
|
|
|
@ -103,19 +103,19 @@ func (u *Container) shouldReveal() bool {
|
||||||
|
|
||||||
func (u *Container) Reset() {
|
func (u *Container) Reset() {
|
||||||
u.SetRevealChild(false)
|
u.SetRevealChild(false)
|
||||||
|
u.State.ID = ""
|
||||||
u.State.Name.Stop()
|
u.State.Name.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update is not thread-safe.
|
// Update is not thread-safe.
|
||||||
func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) {
|
func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) {
|
||||||
// Set the fallback username.
|
u.State.ID = session.ID()
|
||||||
u.State.Name.BindNamer(u.main, "destroy", session)
|
|
||||||
// Reveal the name if it's not empty.
|
|
||||||
u.SetRevealChild(true)
|
u.SetRevealChild(true)
|
||||||
|
|
||||||
// Does messenger implement Nicknamer? If yes, use it.
|
|
||||||
if nicknamer := messenger.AsNicknamer(); nicknamer != nil {
|
if nicknamer := messenger.AsNicknamer(); nicknamer != nil {
|
||||||
u.State.Name.BindNamer(u.main, "destroy", nicknamer)
|
u.State.Name.BindNamer(u.main, "destroy", nicknamer)
|
||||||
|
} else {
|
||||||
|
u.State.Name.BindNamer(u.main, "destroy", session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -238,11 +238,12 @@ func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
|
||||||
section.Box.PackStart(section.Body, false, false, 0)
|
section.Box.PackStart(section.Body, false, false, 0)
|
||||||
section.Box.Show()
|
section.Box.Show()
|
||||||
|
|
||||||
var members = map[string]*Member{}
|
members := map[string]*Member{}
|
||||||
|
|
||||||
// On row click, show the mention popup if any.
|
// On row click, show the mention popup if any.
|
||||||
section.Body.Connect("row-activated", func(_ *gtk.ListBox, r *gtk.ListBoxRow) {
|
section.Body.Connect("row-activated", func(_ *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||||
var i = r.GetIndex()
|
i := r.GetIndex()
|
||||||
|
|
||||||
// Cold path; we can afford searching in the map.
|
// Cold path; we can afford searching in the map.
|
||||||
for _, member := range members {
|
for _, member := range members {
|
||||||
if member.ListBoxRow.GetIndex() == i {
|
if member.ListBoxRow.GetIndex() == i {
|
||||||
|
@ -253,6 +254,7 @@ func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
|
||||||
|
|
||||||
section.name.QueueNamer(context.Background(), sect)
|
section.name.QueueNamer(context.Background(), sect)
|
||||||
section.Header.Connect("destroy", section.name.Stop)
|
section.Header.Connect("destroy", section.name.Stop)
|
||||||
|
section.Members = members
|
||||||
|
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
|
@ -328,12 +330,30 @@ var memberBoxCSS = primitives.PrepareClassCSS("member-box", `
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
var avatarMemberCSS = primitives.PrepareClassCSS("avatar-member", `
|
var avatarBoxMemberCSS = primitives.PrepareClassCSS("avatar-box-member", `
|
||||||
.avatar-member {
|
.avatar-box-member {
|
||||||
padding-right: 10px;
|
margin-right: 10px;
|
||||||
|
padding: 2px;
|
||||||
|
border: 1.5px solid;
|
||||||
|
border-color: #747F8D; /* Offline Grey */
|
||||||
|
border-radius: 99px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-box-member.online {
|
||||||
|
border-color: #43B581;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-box-member.busy {
|
||||||
|
border-color: #F04747;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-box-member.idle {
|
||||||
|
border-color: #FAA61A;
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
var labelMemberCSS = primitives.PrepareClassCSS("label-member", ``)
|
||||||
|
|
||||||
func NewMember(member cchat.ListMember) *Member {
|
func NewMember(member cchat.ListMember) *Member {
|
||||||
m := Member{}
|
m := Member{}
|
||||||
|
|
||||||
|
@ -341,33 +361,44 @@ func NewMember(member cchat.ListMember) *Member {
|
||||||
evb.AddEvents(int(gdk.EVENT_ENTER_NOTIFY) | int(gdk.EVENT_LEAVE_NOTIFY))
|
evb.AddEvents(int(gdk.EVENT_ENTER_NOTIFY) | int(gdk.EVENT_LEAVE_NOTIFY))
|
||||||
evb.Show()
|
evb.Show()
|
||||||
|
|
||||||
m.Avatar = roundimage.NewStillImage(evb, 9999)
|
m.Avatar = roundimage.NewStillImage(evb, 0)
|
||||||
m.Avatar.SetSize(AvatarSize)
|
m.Avatar.SetSize(AvatarSize)
|
||||||
m.Avatar.SetPlaceholderIcon("user-info-symbolic", AvatarSize)
|
m.Avatar.SetPlaceholderIcon("user-info-symbolic", AvatarSize)
|
||||||
m.Avatar.Show()
|
m.Avatar.Show()
|
||||||
avatarMemberCSS(m.Avatar)
|
|
||||||
|
|
||||||
rich.BindRoundImage(m.Avatar, &m.name, true)
|
rich.BindRoundImage(m.Avatar, &m.name, true)
|
||||||
|
|
||||||
|
avaBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
avaBox.SetVAlign(gtk.ALIGN_CENTER)
|
||||||
|
avaBox.PackStart(m.Avatar, false, false, 0)
|
||||||
|
avaBox.Show()
|
||||||
|
avatarBoxMemberCSS(avaBox)
|
||||||
|
|
||||||
m.Name = rich.NewLabel(&m.name)
|
m.Name = rich.NewLabel(&m.name)
|
||||||
m.Name.SetUseMarkup(true)
|
m.Name.SetUseMarkup(true)
|
||||||
m.Name.SetXAlign(0)
|
m.Name.SetXAlign(0)
|
||||||
m.Name.SetEllipsize(pango.ELLIPSIZE_END)
|
m.Name.SetEllipsize(pango.ELLIPSIZE_END)
|
||||||
m.Name.Show()
|
m.Name.Show()
|
||||||
|
labelMemberCSS(m.Name)
|
||||||
|
|
||||||
|
// Keep track of the current status class to replace.
|
||||||
|
var statusClass string
|
||||||
|
styler, _ := avaBox.GetStyleContext()
|
||||||
|
|
||||||
m.Name.SetRenderer(func(rich text.Rich) markup.RenderOutput {
|
m.Name.SetRenderer(func(rich text.Rich) markup.RenderOutput {
|
||||||
out := markup.RenderCmplx(rich)
|
out := markup.RenderCmplxWithConfig(rich, markup.RenderConfig{
|
||||||
|
NoMentionLinks: true,
|
||||||
|
})
|
||||||
|
|
||||||
if m.status != cchat.StatusUnknown {
|
if statusClass != "" {
|
||||||
out.Markup = fmt.Sprintf(
|
styler.RemoveClass(statusClass)
|
||||||
`<span color="#%06X" size="large">●</span> %s`,
|
|
||||||
statusColors(member.Status()), out.Markup,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusClass = statusClassName(m.status)
|
||||||
|
styler.AddClass(statusClass)
|
||||||
|
|
||||||
if !m.second.IsEmpty() {
|
if !m.second.IsEmpty() {
|
||||||
out.Markup += fmt.Sprintf(
|
out.Markup += fmt.Sprintf(
|
||||||
"\n"+`<span alpha="85%%"><sup>%s</sup></span>`,
|
`<span alpha="85%%" size="small">`+"\n"+`%s</span>`,
|
||||||
markup.Render(m.second),
|
markup.Render(m.second),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -376,7 +407,7 @@ func NewMember(member cchat.ListMember) *Member {
|
||||||
})
|
})
|
||||||
|
|
||||||
m.Main, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
m.Main, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
m.Main.PackStart(m.Avatar, false, false, 0)
|
m.Main.PackStart(avaBox, false, false, 0)
|
||||||
m.Main.PackStart(m.Name, true, true, 0)
|
m.Main.PackStart(m.Name, true, true, 0)
|
||||||
m.Main.Show()
|
m.Main.Show()
|
||||||
memberBoxCSS(m.Main)
|
memberBoxCSS(m.Main)
|
||||||
|
@ -392,6 +423,25 @@ func NewMember(member cchat.ListMember) *Member {
|
||||||
return &m
|
return &m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusClassName(status cchat.Status) string {
|
||||||
|
switch status {
|
||||||
|
case cchat.StatusOnline:
|
||||||
|
return "online"
|
||||||
|
case cchat.StatusBusy:
|
||||||
|
return "busy"
|
||||||
|
case cchat.StatusAway:
|
||||||
|
fallthrough
|
||||||
|
case cchat.StatusIdle:
|
||||||
|
return "idle"
|
||||||
|
case cchat.StatusInvisible:
|
||||||
|
fallthrough
|
||||||
|
case cchat.StatusOffline:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var noMentionLinks = markup.RenderConfig{
|
var noMentionLinks = markup.RenderConfig{
|
||||||
NoMentionLinks: true,
|
NoMentionLinks: true,
|
||||||
NoReferencing: true,
|
NoReferencing: true,
|
||||||
|
|
|
@ -19,10 +19,11 @@ import (
|
||||||
// made for containers to override; methods not meant to be override are not
|
// made for containers to override; methods not meant to be override are not
|
||||||
// exposed and will be done directly on the State.
|
// exposed and will be done directly on the State.
|
||||||
type Container interface {
|
type Container interface {
|
||||||
// Unwrap unwraps the message container and, if revert is true, revert the
|
// Unwrap returns the internal message state.
|
||||||
// state to a clean version. Containers must implement this method by
|
Unwrap() *State
|
||||||
// itself.
|
// Revert unwraps and reverts all widget changes to the internal state then
|
||||||
Unwrap(revert bool) *State
|
// returns that state.
|
||||||
|
Revert() *State
|
||||||
|
|
||||||
// UpdateContent updates the underlying content widget.
|
// UpdateContent updates the underlying content widget.
|
||||||
UpdateContent(content text.Rich, edited bool)
|
UpdateContent(content text.Rich, edited bool)
|
||||||
|
@ -50,8 +51,7 @@ type State struct {
|
||||||
MenuItems []menu.Item
|
MenuItems []menu.Item
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewState creates a new message state with the given ID and nonce. It does not
|
// NewState creates a new message state with the given MessageCreate.
|
||||||
// update the widgets, so FillContainer should be called afterwards.
|
|
||||||
func NewState(msg cchat.MessageCreate) *State {
|
func NewState(msg cchat.MessageCreate) *State {
|
||||||
author := msg.Author()
|
author := msg.Author()
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ func NewState(msg cchat.MessageCreate) *State {
|
||||||
c.ID = msg.ID()
|
c.ID = msg.ID()
|
||||||
c.Time = msg.Time()
|
c.Time = msg.Time()
|
||||||
c.Nonce = msg.Nonce()
|
c.Nonce = msg.Nonce()
|
||||||
|
c.UpdateContent(msg.Content(), false)
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
@ -69,8 +70,7 @@ func NewState(msg cchat.MessageCreate) *State {
|
||||||
// immediately afterwards; it is invalid once the state is used.
|
// immediately afterwards; it is invalid once the state is used.
|
||||||
func NewEmptyState() *State {
|
func NewEmptyState() *State {
|
||||||
ctbody := labeluri.NewLabel(text.Rich{})
|
ctbody := labeluri.NewLabel(text.Rich{})
|
||||||
ctbody.SetVExpand(true)
|
ctbody.SetHAlign(gtk.ALIGN_FILL)
|
||||||
ctbody.SetHAlign(gtk.ALIGN_START)
|
|
||||||
ctbody.SetEllipsize(pango.ELLIPSIZE_NONE)
|
ctbody.SetEllipsize(pango.ELLIPSIZE_NONE)
|
||||||
ctbody.SetLineWrap(true)
|
ctbody.SetLineWrap(true)
|
||||||
ctbody.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
ctbody.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||||
|
@ -84,10 +84,11 @@ func NewEmptyState() *State {
|
||||||
|
|
||||||
// Wrap the content label inside a content box.
|
// Wrap the content label inside a content box.
|
||||||
ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
ctbox.SetHExpand(true)
|
|
||||||
ctbox.PackStart(ctbody, false, false, 0)
|
ctbox.PackStart(ctbody, false, false, 0)
|
||||||
|
ctbox.SetHAlign(gtk.ALIGN_FILL)
|
||||||
ctbox.Show()
|
ctbox.Show()
|
||||||
|
|
||||||
|
// Box that belongs to the implementations of messages.
|
||||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
box.Show()
|
box.Show()
|
||||||
|
|
||||||
|
@ -122,13 +123,38 @@ func NewEmptyState() *State {
|
||||||
return gc
|
return gc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearBox clears the state's widget container.
|
||||||
|
func (m *State) ClearBox() {
|
||||||
|
primitives.RemoveChildren(m)
|
||||||
|
m.SetClass("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// // For debugging use only.
|
||||||
|
// func (m *State) PackStart(child gtk.IWidget, expand bool, fill bool, padding uint) {
|
||||||
|
// paths := make([]string, 0, 5)
|
||||||
|
// for i := 1; i < 5; i++ {
|
||||||
|
// _, file, line, ok := runtime.Caller(i)
|
||||||
|
// if !ok {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// paths = append(paths, fmt.Sprintf("%s:%d", filepath.Base(file), line))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// log.Println("child packstart", m.ID, "at", strings.Join(paths, " < "))
|
||||||
|
// m.Box.PackStart(child, expand, fill, padding)
|
||||||
|
// }
|
||||||
|
|
||||||
// SetClass sets the internal row's class.
|
// SetClass sets the internal row's class.
|
||||||
func (m *State) SetClass(class string) {
|
func (m *State) SetClass(class string) {
|
||||||
if m.class != "" {
|
if m.class != "" {
|
||||||
primitives.RemoveClass(m.Row, m.class)
|
primitives.RemoveClass(m.Row, m.class)
|
||||||
}
|
}
|
||||||
|
|
||||||
primitives.AddClass(m.Row, class)
|
if class != "" {
|
||||||
|
primitives.AddClass(m.Row, class)
|
||||||
|
}
|
||||||
|
|
||||||
m.class = class
|
m.class = class
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,3 +179,6 @@ func (m *State) UpdateContent(content text.Rich, edited bool) {
|
||||||
func (m *State) Focusable() gtk.IWidget {
|
func (m *State) Focusable() gtk.IWidget {
|
||||||
return m.Content
|
return m.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap returns itself.
|
||||||
|
func (m *State) Unwrap() *State { return m }
|
||||||
|
|
|
@ -45,14 +45,10 @@ type PresendState struct {
|
||||||
uploads *attachment.MessageUploader
|
uploads *attachment.MessageUploader
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var _ Presender = (*PresendState)(nil)
|
||||||
_ Presender = (*PresendState)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
type SendMessageData struct {
|
// NewPresendState creates a new presend state. The caller must call one of the
|
||||||
}
|
// state setters, usually SetLoading.
|
||||||
|
|
||||||
// NewPresendState creates a new presend state.
|
|
||||||
func NewPresendState(self *Author, msg PresendMessage) *PresendState {
|
func NewPresendState(self *Author, msg PresendMessage) *PresendState {
|
||||||
c := NewEmptyState()
|
c := NewEmptyState()
|
||||||
c.Author = self
|
c.Author = self
|
||||||
|
@ -64,7 +60,7 @@ func NewPresendState(self *Author, msg PresendMessage) *PresendState {
|
||||||
presend: msg,
|
presend: msg,
|
||||||
uploads: attachment.NewMessageUploader(msg.Files()),
|
uploads: attachment.NewMessageUploader(msg.Files()),
|
||||||
}
|
}
|
||||||
p.SetLoading()
|
// p.SetLoading()
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ func (mc *MessageControl) Enable(msg container.MessageRow, names MessageItemName
|
||||||
mc.SetSensitive(true)
|
mc.SetSensitive(true)
|
||||||
mc.SetRevealChild(true && !mc.hide)
|
mc.SetRevealChild(true && !mc.hide)
|
||||||
|
|
||||||
unwrap := msg.Unwrap(false)
|
unwrap := msg.Unwrap()
|
||||||
|
|
||||||
mc.Reply.bind(menu.FindItemFunc(unwrap.MenuItems, names.Reply))
|
mc.Reply.bind(menu.FindItemFunc(unwrap.MenuItems, names.Reply))
|
||||||
mc.Edit.bind(menu.FindItemFunc(unwrap.MenuItems, names.Edit))
|
mc.Edit.bind(menu.FindItemFunc(unwrap.MenuItems, names.Edit))
|
||||||
|
|
|
@ -114,9 +114,10 @@ func NewView(c Controller) *View {
|
||||||
view.MsgBox.Show()
|
view.MsgBox.Show()
|
||||||
|
|
||||||
view.Scroller = autoscroll.NewScrolledWindow()
|
view.Scroller = autoscroll.NewScrolledWindow()
|
||||||
view.Scroller.Add(view.MsgBox)
|
view.Scroller.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||||
view.Scroller.SetVExpand(true)
|
view.Scroller.SetVExpand(true)
|
||||||
view.Scroller.SetHExpand(true)
|
view.Scroller.SetHExpand(true)
|
||||||
|
view.Scroller.Add(view.MsgBox)
|
||||||
view.Scroller.Show()
|
view.Scroller.Show()
|
||||||
messageScroller(view.Scroller)
|
messageScroller(view.Scroller)
|
||||||
|
|
||||||
|
@ -352,12 +353,12 @@ func (v *View) JoinServer(ses *session.Row, srv *server.ServerRow, bc traverse.B
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) FetchBacklog() {
|
func (v *View) FetchBacklog() {
|
||||||
var backlogger = v.state.Backlogger()
|
backlogger := v.state.Backlogger()
|
||||||
if backlogger == nil {
|
if backlogger == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstMsg = v.Container.FirstMessage()
|
firstMsg := container.FirstMessage(v.Container)
|
||||||
if firstMsg == nil {
|
if firstMsg == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -365,12 +366,12 @@ func (v *View) FetchBacklog() {
|
||||||
// Set the window as busy. TODO: loading circles.
|
// Set the window as busy. TODO: loading circles.
|
||||||
v.ctrl.OnMessageBusy()
|
v.ctrl.OnMessageBusy()
|
||||||
|
|
||||||
var done = func() {
|
done := func() {
|
||||||
v.ctrl.OnMessageDone()
|
v.ctrl.OnMessageDone()
|
||||||
v.Container.Highlight(firstMsg)
|
v.Container.Highlight(firstMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
firstID := firstMsg.Unwrap(false).ID
|
firstID := firstMsg.Unwrap().ID
|
||||||
|
|
||||||
gts.Async(func() (func(), error) {
|
gts.Async(func() (func(), error) {
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
|
||||||
|
@ -396,59 +397,62 @@ func (v *View) MessageAuthor(msgID cchat.ID) *message.Author {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg.Unwrap(false).Author
|
return msg.Unwrap().Author
|
||||||
}
|
}
|
||||||
|
|
||||||
// Author returns the author from the message list with the given author ID.
|
// Author returns the author from the message list with the given author ID.
|
||||||
func (v *View) Author(authorID cchat.ID) rich.LabelStateStorer {
|
func (v *View) Author(authorID cchat.ID) rich.LabelStateStorer {
|
||||||
msg := v.Container.FindMessage(func(msg container.MessageRow) bool {
|
msg, _ := v.Container.FindMessage(func(msg container.MessageRow) bool {
|
||||||
return msg.Unwrap(false).Author.ID == authorID
|
return msg.Unwrap().Author.ID == authorID
|
||||||
})
|
})
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
state := msg.Unwrap(false)
|
state := msg.Unwrap()
|
||||||
return &state.Author.Name
|
return &state.Author.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestMessageFrom returns the last message ID with that author.
|
// LatestMessageFrom returns the last message ID with that author.
|
||||||
func (v *View) LatestMessageFrom(userID cchat.ID) container.MessageRow {
|
func (v *View) LatestMessageFrom(userID cchat.ID) container.MessageRow {
|
||||||
return container.LatestMessageFrom(v.Container, userID)
|
msg, _ := container.LatestMessageFrom(v.Container, userID)
|
||||||
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) SendMessage(msg message.PresendMessage) {
|
func (v *View) SendMessage(msg message.PresendMessage) {
|
||||||
state := message.NewPresendState(v.InputView.Username.State, msg)
|
state := message.NewPresendState(v.InputView.Username.State, msg)
|
||||||
msgr := v.Container.NewPresendMessage(state)
|
msgr := v.Container.NewPresendMessage(state)
|
||||||
|
v.retryMessage(state, msgr)
|
||||||
v.retryMessage(msgr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// retryMessage sends the message.
|
// retryMessage sends the message.
|
||||||
func (v *View) retryMessage(presend container.PresendMessageRow) {
|
func (v *View) retryMessage(state *message.PresendState, presend container.PresendMessageRow) {
|
||||||
var sender = v.InputView.Sender
|
var sender = v.InputView.Sender
|
||||||
if sender == nil {
|
if sender == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the message is set to loading.
|
||||||
|
presend.SetLoading()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := sender.Send(presend.SendingMessage()); err != nil {
|
err := sender.Send(presend.SendingMessage())
|
||||||
// Set the message's state to errored again, but we don't need to
|
if err == nil {
|
||||||
// rebind the menu.
|
return
|
||||||
gts.ExecAsync(func() {
|
|
||||||
// Set the retry message.
|
|
||||||
presend.SetSentError(err)
|
|
||||||
// Only attach the menu once. Further retries do not need to be
|
|
||||||
// reattached.
|
|
||||||
state := presend.Unwrap(false)
|
|
||||||
state.MenuItems = []menu.Item{
|
|
||||||
menu.SimpleItem("Retry", func() {
|
|
||||||
presend.SetLoading()
|
|
||||||
v.retryMessage(presend)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the message's state to errored again, but we don't need to rebind
|
||||||
|
// the menu.
|
||||||
|
gts.ExecAsync(func() {
|
||||||
|
presend.SetSentError(err)
|
||||||
|
|
||||||
|
state.MenuItems = []menu.Item{
|
||||||
|
menu.SimpleItem("Retry", func() {
|
||||||
|
presend.SetLoading()
|
||||||
|
v.retryMessage(state, presend)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,7 +465,7 @@ var messageItemNames = MessageItemNames{
|
||||||
// BindMenu attaches the menu constructor into the message with the needed
|
// BindMenu attaches the menu constructor into the message with the needed
|
||||||
// states and callbacks.
|
// states and callbacks.
|
||||||
func (v *View) BindMenu(msg container.MessageRow) {
|
func (v *View) BindMenu(msg container.MessageRow) {
|
||||||
state := msg.Unwrap(false)
|
state := msg.Unwrap()
|
||||||
|
|
||||||
// Add 1 for the edit menu item.
|
// Add 1 for the edit menu item.
|
||||||
var mitems = []menu.Item{
|
var mitems = []menu.Item{
|
||||||
|
|
|
@ -173,17 +173,15 @@ func (i *Image) SetImageURLInto(url string, otherImage httputil.ImageContainer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if i.icon.name != "" {
|
|
||||||
primitives.SetImageIcon(i, i.icon.name, i.icon.size)
|
|
||||||
goto noImage
|
|
||||||
}
|
|
||||||
|
|
||||||
if i.ifNone != nil {
|
if i.ifNone != nil {
|
||||||
i.ifNone(ctx)
|
i.ifNone(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
noImage:
|
if i.icon.name != "" {
|
||||||
|
primitives.SetImageIcon(i, i.icon.name, i.icon.size)
|
||||||
|
}
|
||||||
|
|
||||||
i.Image.SetFromPixbuf(nil)
|
i.Image.SetFromPixbuf(nil)
|
||||||
i.cancel()
|
i.cancel()
|
||||||
}
|
}
|
||||||
|
@ -216,9 +214,14 @@ func (i *Image) drawer(image *gtk.Image, cc *cairo.Context) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
a := image.GetAllocation()
|
var w, h float64
|
||||||
w := float64(a.GetWidth())
|
if reqW, reqH := image.GetSizeRequest(); reqW > 0 && reqH > 0 {
|
||||||
h := float64(a.GetHeight())
|
w = float64(reqW)
|
||||||
|
h = float64(reqH)
|
||||||
|
} else {
|
||||||
|
w = float64(image.GetAllocatedWidth())
|
||||||
|
h = float64(image.GetAllocatedHeight())
|
||||||
|
}
|
||||||
|
|
||||||
min := w
|
min := w
|
||||||
// Use the largest side for radius calculation.
|
// Use the largest side for radius calculation.
|
||||||
|
|
|
@ -62,29 +62,23 @@ func (namec *NameContainer) Stop() {
|
||||||
if namec.state != nil {
|
if namec.state != nil {
|
||||||
namec.state.Stop()
|
namec.state.Stop()
|
||||||
namec.LabelState.setLabel(text.Plain(""))
|
namec.LabelState.setLabel(text.Plain(""))
|
||||||
|
} else {
|
||||||
|
namec.state = &containerState{
|
||||||
|
current: func() {},
|
||||||
|
stop: func() {},
|
||||||
|
}
|
||||||
|
runtime.SetFinalizer(namec.state, (*containerState).Stop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *containerState) Stop() {
|
func (state *containerState) Stop() {
|
||||||
if state.current != nil {
|
state.current()
|
||||||
state.current()
|
state.stop()
|
||||||
state.current = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.stop != nil {
|
|
||||||
state.stop()
|
|
||||||
state.stop = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueueNamer tries using the namer in the background and queue the setter onto
|
// QueueNamer tries using the namer in the background and queue the setter onto
|
||||||
// the next GLib loop iteration.
|
// the next GLib loop iteration.
|
||||||
func (namec *NameContainer) QueueNamer(ctx context.Context, namer cchat.Namer) {
|
func (namec *NameContainer) QueueNamer(ctx context.Context, namer cchat.Namer) {
|
||||||
if namec.state == nil {
|
|
||||||
namec.state = &containerState{}
|
|
||||||
runtime.SetFinalizer(namec.state, (*containerState).Stop)
|
|
||||||
}
|
|
||||||
|
|
||||||
namec.Stop()
|
namec.Stop()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
@ -98,7 +92,6 @@ func (namec *NameContainer) QueueNamer(ctx context.Context, namer cchat.Namer) {
|
||||||
|
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
namec.state.current()
|
namec.state.current()
|
||||||
namec.state.current = nil
|
|
||||||
namec.state.stop = stop
|
namec.state.stop = stop
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
@ -115,11 +108,11 @@ func (namec *NameContainer) BindNamer(w primitives.Connector, sig string, namer
|
||||||
// namec.Stop()
|
// namec.Stop()
|
||||||
|
|
||||||
// ctx, cancel := context.WithCancel(context.Background())
|
// ctx, cancel := context.WithCancel(context.Background())
|
||||||
// namec.current = cancel
|
// namec.state.current = cancel
|
||||||
|
|
||||||
// // TODO: this might leak, because namec.Stop references the fns list which
|
// // TODO: this might leak, because namec.Stop references the fns list which
|
||||||
// // might reference w indirectly.
|
// // might reference w indirectly.
|
||||||
// w.Connect(sig, namec.Stop)
|
// handle := w.Connect(sig, namec.Stop)
|
||||||
|
|
||||||
// go func() {
|
// go func() {
|
||||||
// stop, err := namer.Name(ctx, namec)
|
// stop, err := namer.Name(ctx, namec)
|
||||||
|
@ -128,9 +121,18 @@ func (namec *NameContainer) BindNamer(w primitives.Connector, sig string, namer
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// gts.ExecAsync(func() {
|
// gts.ExecAsync(func() {
|
||||||
// namec.current()
|
// namec.state.current()
|
||||||
// namec.current = nil
|
|
||||||
// namec.stop = stop // nil is OK.
|
// if stop != nil {
|
||||||
|
// namec.state.stop = func() {
|
||||||
|
// w.HandlerDisconnect(handle)
|
||||||
|
// stop()
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// namec.state.stop = func() {
|
||||||
|
// w.HandlerDisconnect(handle)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
// })
|
// })
|
||||||
// }()
|
// }()
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,17 +124,13 @@ func (h *Header) SetSessionMenu(s *session.Row) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type sizeBinder interface {
|
type sizeBinder interface {
|
||||||
primitives.Connector
|
gtk.IWidget
|
||||||
GetAllocatedWidth() int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ sizeBinder = (*List)(nil)
|
var _ sizeBinder = (*List)(nil)
|
||||||
|
|
||||||
func (h *Header) AppMenuBindSize(c sizeBinder) {
|
func (h *Header) AppMenuBindSize(c sizeBinder) {
|
||||||
update := func(c sizeBinder) {
|
sg, _ := gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL)
|
||||||
h.AppMenu.SetSizeRequest(c.GetAllocatedWidth(), -1)
|
sg.AddWidget(c)
|
||||||
}
|
sg.AddWidget(h.AppMenu)
|
||||||
|
|
||||||
c.Connect("size-allocate", update)
|
|
||||||
update(c)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/humanize"
|
"github.com/diamondburned/cchat-gtk/internal/humanize"
|
||||||
|
@ -11,8 +9,8 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/serverpane"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/serverpane"
|
||||||
|
"github.com/diamondburned/handy"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/gotk3/gotk3/pango"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const FaceSize = 48 // gtk.ICON_SIZE_DIALOG
|
const FaceSize = 48 // gtk.ICON_SIZE_DIALOG
|
||||||
|
@ -212,33 +210,20 @@ func (s *Servers) setFailed(err error) {
|
||||||
s.Stack.Remove(w)
|
s.Stack.Remove(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a BLANK label for padding.
|
|
||||||
ltop, _ := gtk.LabelNew("")
|
|
||||||
ltop.Show()
|
|
||||||
|
|
||||||
// Create a retry button.
|
// Create a retry button.
|
||||||
btn, _ := gtk.ButtonNewFromIconName("view-refresh-symbolic", gtk.ICON_SIZE_DIALOG)
|
btn, _ := gtk.ButtonNewFromIconName("view-refresh-symbolic", gtk.ICON_SIZE_BUTTON)
|
||||||
|
btn.SetLabel("Retry")
|
||||||
|
btn.Connect("clicked", s.load)
|
||||||
btn.Show()
|
btn.Show()
|
||||||
btn.Connect("clicked", func(interface{}) { s.load() })
|
|
||||||
|
|
||||||
// Create a bottom label for the error itself.
|
page := handy.StatusPageNew()
|
||||||
lerr, _ := gtk.LabelNew("")
|
page.SetTitle("Error")
|
||||||
lerr.SetSingleLineMode(true)
|
page.SetIconName("dialog-error")
|
||||||
lerr.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
page.SetTooltipText(err.Error())
|
||||||
lerr.SetMarkup(fmt.Sprintf(
|
page.SetDescription(humanize.Error(err))
|
||||||
`<span color="red"><b>Error:</b> %s</span>`,
|
page.Add(btn)
|
||||||
humanize.Error(err),
|
|
||||||
))
|
|
||||||
lerr.Show()
|
|
||||||
|
|
||||||
// Add these items into the box.
|
s.Stack.AddNamed(page, "error")
|
||||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
|
||||||
b.PackStart(ltop, false, false, 0)
|
|
||||||
b.PackStart(btn, false, false, 10) // pad
|
|
||||||
b.PackStart(lerr, false, false, 0)
|
|
||||||
b.Show()
|
|
||||||
|
|
||||||
s.Stack.AddNamed(b, "error")
|
|
||||||
s.Stack.SetVisibleChildName("error")
|
s.Stack.SetVisibleChildName("error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
15
profile.go
15
profile.go
|
@ -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))
|
|
||||||
}()
|
|
||||||
}
|
|
24
shell.nix
24
shell.nix
|
@ -1,21 +1,19 @@
|
||||||
{ pkgs ? import <nixpkgs> {} }:
|
{ unstable ? import <unstable> {} }:
|
||||||
|
|
||||||
pkgs.stdenv.mkDerivation rec {
|
unstable.stdenv.mkDerivation rec {
|
||||||
name = "cchat-gtk";
|
name = "cchat-gtk";
|
||||||
version = "0.0.2";
|
version = "0.0.2";
|
||||||
|
|
||||||
buildInputs = [
|
buildInputs = with unstable; [
|
||||||
pkgs.libhandy
|
libhandy
|
||||||
pkgs.gnome3.gspell
|
gnome3.gspell
|
||||||
pkgs.gnome3.glib
|
gnome3.glib
|
||||||
pkgs.gnome3.gtk
|
gnome3.gtk
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with unstable; [
|
||||||
pkgconfig go
|
pkgconfig
|
||||||
|
go
|
||||||
|
wrapGAppsHook
|
||||||
];
|
];
|
||||||
|
|
||||||
# Debug flags.
|
|
||||||
CGO_CFLAGS = "-g";
|
|
||||||
CGO_CXXFLAGS = "-g";
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue