Works with cchat v0.3
This commit is contained in:
parent
ba4728c3d6
commit
a10230a8cd
|
@ -1 +1,3 @@
|
||||||
cchat-gtk
|
cchat-gtk
|
||||||
|
.direnv
|
||||||
|
.envrc
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -7,9 +7,9 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200816
|
||||||
require (
|
require (
|
||||||
github.com/Xuanwo/go-locale v0.2.0
|
github.com/Xuanwo/go-locale v0.2.0
|
||||||
github.com/alecthomas/chroma v0.7.3
|
github.com/alecthomas/chroma v0.7.3
|
||||||
github.com/diamondburned/cchat v0.0.49
|
github.com/diamondburned/cchat v0.3.7
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200821041521-647c854d7b5e
|
github.com/diamondburned/cchat-discord v0.0.0-20201015062850-090259a6b4ca
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b
|
github.com/diamondburned/cchat-mock v0.0.0-20201014202453-b9838fab0ab0
|
||||||
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894
|
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894
|
||||||
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4
|
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
|
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
|
||||||
|
@ -23,6 +23,6 @@ require (
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||||
github.com/twmb/murmur3 v1.1.3
|
github.com/twmb/murmur3 v1.1.3
|
||||||
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
|
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.7 // indirect
|
gopkg.in/yaml.v2 v2.2.7 // indirect
|
||||||
)
|
)
|
||||||
|
|
36
go.sum
36
go.sum
|
@ -64,6 +64,25 @@ github.com/diamondburned/cchat v0.0.48 h1:MAzGzKY20JBh/LnirOZVPwbMq07xfqu4Lb4XsV
|
||||||
github.com/diamondburned/cchat v0.0.48/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat v0.0.48/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
github.com/diamondburned/cchat v0.0.49 h1:zP6QvjdRU3UqDZt3rEqjkR/5M68XRVms7htHfE9tLOc=
|
github.com/diamondburned/cchat v0.0.49 h1:zP6QvjdRU3UqDZt3rEqjkR/5M68XRVms7htHfE9tLOc=
|
||||||
github.com/diamondburned/cchat v0.0.49/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat v0.0.49/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
|
github.com/diamondburned/cchat v0.2.11 h1:w4c/6t02htGtVj6yIjznecOGMlkcj0TmmLy+K48gHeM=
|
||||||
|
github.com/diamondburned/cchat v0.2.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.2.12 h1:R4wrBdhELMfhv2Kn3xL/H3ci8UcLXzFRPq1IrY4+js4=
|
||||||
|
github.com/diamondburned/cchat v0.2.12/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.2.13 h1:12xmJ1DpLZTG9icGZSXruCPT2BylOdhgXfKKwbqUXx4=
|
||||||
|
github.com/diamondburned/cchat v0.2.13/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.2.14 h1:mxcwre0LMjimrN5UAZVmerBqg9p2OxgjF27fJ1ASMjw=
|
||||||
|
github.com/diamondburned/cchat v0.2.14/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.2.15 h1:GYKD4VTrWCf1zIsFmyDVsUYaLjvVhgEh7qrg4KtaM0k=
|
||||||
|
github.com/diamondburned/cchat v0.2.15/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.3.1 h1:7NbVjT50dmLxcHPm+eDFF5jcaZw3t/9IdSEkZ/md1Rg=
|
||||||
|
github.com/diamondburned/cchat v0.3.1/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.3.2 h1:KcaWAN5qztKsSVsVGAWE4Mr779fOFLwLkxqlGh2bLo8=
|
||||||
|
github.com/diamondburned/cchat v0.3.2/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.3.3 h1:FFdcahDUGGP/h9BvVjoYOKgNXSRTQS+6Blb8keQmxVw=
|
||||||
|
github.com/diamondburned/cchat v0.3.3/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.3.5/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
|
github.com/diamondburned/cchat v0.3.7 h1:0t3FkbzC/pBRAR3w0uYznJ+7dYqcR1M48a9wgz4JkIg=
|
||||||
|
github.com/diamondburned/cchat v0.3.7/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401 h1:llmx/8UiJoTcHUw+GE5/rESVVmmnLh1HEPx3wRj+oQY=
|
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401 h1:llmx/8UiJoTcHUw+GE5/rESVVmmnLh1HEPx3wRj+oQY=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401/go.mod h1:+hSrIVYj5tIPLAorDsHj2Tbt2fWlZtOanzfEUHX53HM=
|
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401/go.mod h1:+hSrIVYj5tIPLAorDsHj2Tbt2fWlZtOanzfEUHX53HM=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200730000036-2c93cdc1974e h1:EA5Vg0x57qLURJP80XhABBW+X0sbQSh2gw5qvPbZTs4=
|
github.com/diamondburned/cchat-discord v0.0.0-20200730000036-2c93cdc1974e h1:EA5Vg0x57qLURJP80XhABBW+X0sbQSh2gw5qvPbZTs4=
|
||||||
|
@ -84,8 +103,24 @@ github.com/diamondburned/cchat-discord v0.0.0-20200820222718-68cfafc4c318 h1:mRG
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200820222718-68cfafc4c318/go.mod h1:rhUseXyWXTVw0Da8edbQMHU9I4LRQ2zcRB3zRqg/oe4=
|
github.com/diamondburned/cchat-discord v0.0.0-20200820222718-68cfafc4c318/go.mod h1:rhUseXyWXTVw0Da8edbQMHU9I4LRQ2zcRB3zRqg/oe4=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200821041521-647c854d7b5e h1:higtJiL7t6owP2dVAwJxItnpsD1MUypWDVant2uYv6g=
|
github.com/diamondburned/cchat-discord v0.0.0-20200821041521-647c854d7b5e h1:higtJiL7t6owP2dVAwJxItnpsD1MUypWDVant2uYv6g=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200821041521-647c854d7b5e/go.mod h1:rhUseXyWXTVw0Da8edbQMHU9I4LRQ2zcRB3zRqg/oe4=
|
github.com/diamondburned/cchat-discord v0.0.0-20200821041521-647c854d7b5e/go.mod h1:rhUseXyWXTVw0Da8edbQMHU9I4LRQ2zcRB3zRqg/oe4=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201007015315-da520786d74b h1:IJYC5vKdT9zTX/vLRXKIpv9xC6FNVq13O3X5ndSsN6g=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201007015315-da520786d74b/go.mod h1:Bp7CERMjWVJf/Rv8pO8pdcg/ZLuvJ24TenDAzfW+Nl8=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201009070751-b6694ea24a39 h1:P1KcSdD1a8fszRH3exaT9B63OtKx1MKTR6YllbiXRXQ=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201009070751-b6694ea24a39/go.mod h1:ijG0kx3DLVygYUlhVPvvBAlLW8cNtUuXdFtAUtigvOw=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201009173316-1907986ceb08 h1:iytskZ4dvc6KLlMDCpFcTIB7nIZkbprjDnaL2bCkduE=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201009173316-1907986ceb08/go.mod h1:BF8CJaW6rdYDGjFd2qXODS5nSu9vvW7OehgkXIB8B0M=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201015062850-090259a6b4ca h1:36MnUdiunaz4hsqDO0313Nc03y59PzIPZtmEF8gUeCg=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201015062850-090259a6b4ca/go.mod h1:S0PDR6aj2qE871JSy94YvwtprQJCWwkIJWzRu7S1Asc=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b h1:sq0MXjJc3yAOZvuolRxOpKQNvpMLyTmsECxQqdYgF5E=
|
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b h1:sq0MXjJc3yAOZvuolRxOpKQNvpMLyTmsECxQqdYgF5E=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b/go.mod h1:+bAf0m2o5qH54DmYJ/lR1HeITV53ol0JaoKyFFx3m3E=
|
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b/go.mod h1:+bAf0m2o5qH54DmYJ/lR1HeITV53ol0JaoKyFFx3m3E=
|
||||||
|
github.com/diamondburned/cchat-mock v0.0.0-20201004204741-b841407af381 h1:8JWNJMgoa3fL2py3gXSeC3NiAC+39EZp+JmvaoDBTUU=
|
||||||
|
github.com/diamondburned/cchat-mock v0.0.0-20201004204741-b841407af381/go.mod h1:dObDshcI3LXSicnuBBoRiCV6j0H5FZwp6wq4yANMdyQ=
|
||||||
|
github.com/diamondburned/cchat-mock v0.0.0-20201009070609-ab7eccf48e52 h1:0XES1llczz7181MtWsmMBvSibBZg9CAlGx+eDpyvzdM=
|
||||||
|
github.com/diamondburned/cchat-mock v0.0.0-20201009070609-ab7eccf48e52/go.mod h1:qDKTtPsdMeAmOV1QWwIII1hBzjcCZOXsbxDYoyuw2eo=
|
||||||
|
github.com/diamondburned/cchat-mock v0.0.0-20201009173002-83501e8aad33 h1:1rw4gQwAPAR47OLB06st0RP77jCrH+29EH/Xgu0otKI=
|
||||||
|
github.com/diamondburned/cchat-mock v0.0.0-20201009173002-83501e8aad33/go.mod h1:tshTM0VduaKpzqYnVYdAozXVk/dBkEkoyM3KXua+cIk=
|
||||||
|
github.com/diamondburned/cchat-mock v0.0.0-20201014202453-b9838fab0ab0 h1:GwceonhYEE6QZzqMTCc/OsBJ+CZ9omWzN/4if+e62uA=
|
||||||
|
github.com/diamondburned/cchat-mock v0.0.0-20201014202453-b9838fab0ab0/go.mod h1:hYNki0Ic/d7zFVXTJIjp/td1W4OpxDNcVY8layxgTyc=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI=
|
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20200816224505-3cd69b83a48a h1:wEldljb421/Jp84RNb0zBfqmiWt/TTQzUE6R1ap6UuQ=
|
github.com/diamondburned/gotk3 v0.0.0-20200816224505-3cd69b83a48a h1:wEldljb421/Jp84RNb0zBfqmiWt/TTQzUE6R1ap6UuQ=
|
||||||
|
@ -138,6 +173,7 @@ github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||||
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
||||||
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||||
|
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
|
|
@ -27,14 +27,8 @@ type Session struct {
|
||||||
func ConvertSession(ses cchat.Session) *Session {
|
func ConvertSession(ses cchat.Session) *Session {
|
||||||
var name = ses.Name().Content
|
var name = ses.Name().Content
|
||||||
|
|
||||||
saver, ok := ses.(cchat.SessionSaver)
|
saver := ses.AsSessionSaver()
|
||||||
if !ok {
|
if saver == nil {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := saver.Save()
|
|
||||||
if err != nil {
|
|
||||||
log.Error(errors.Wrapf(err, "Failed to save session ID %s (%s)", ses.ID(), name))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +41,7 @@ func ConvertSession(ses cchat.Session) *Session {
|
||||||
return &Session{
|
return &Session{
|
||||||
ID: ses.ID(),
|
ID: ses.ID(),
|
||||||
Name: name,
|
Name: name,
|
||||||
Data: s,
|
Data: saver.SaveSession(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,6 @@ type Container interface {
|
||||||
|
|
||||||
// Thread-safe methods.
|
// Thread-safe methods.
|
||||||
cchat.MessagesContainer
|
cchat.MessagesContainer
|
||||||
cchat.MessagePrepender
|
|
||||||
|
|
||||||
// Thread-unsafe methods.
|
// Thread-unsafe methods.
|
||||||
CreateMessageUnsafe(cchat.MessageCreate)
|
CreateMessageUnsafe(cchat.MessageCreate)
|
||||||
|
@ -73,7 +72,7 @@ type Controller interface {
|
||||||
Bottomed() bool
|
Bottomed() bool
|
||||||
// AuthorEvent is called on message create/update. This is used to update
|
// AuthorEvent is called on message create/update. This is used to update
|
||||||
// the typer state.
|
// the typer state.
|
||||||
AuthorEvent(a cchat.MessageAuthor)
|
AuthorEvent(a cchat.Author)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor is an interface for making custom message implementations which
|
// Constructor is an interface for making custom message implementations which
|
||||||
|
|
|
@ -70,10 +70,10 @@ func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
|
||||||
author := msg.Author()
|
author := msg.Author()
|
||||||
|
|
||||||
// Try and reuse an existing avatar if the author has one.
|
// Try and reuse an existing avatar if the author has one.
|
||||||
if avatarURL, ok := author.(cchat.MessageAuthorAvatar); ok {
|
if avatarURL := author.Avatar(); avatarURL != "" {
|
||||||
// Try reusing the avatar, but fetch it from the interndet if we can't
|
// Try reusing the avatar, but fetch it from the interndet if we can't
|
||||||
// reuse. The reuse function does this for us.
|
// reuse. The reuse function does this for us.
|
||||||
c.reuseAvatar(author.ID(), avatarURL.Avatar(), full)
|
c.reuseAvatar(author.ID(), author.Avatar(), full)
|
||||||
}
|
}
|
||||||
|
|
||||||
return full
|
return full
|
||||||
|
|
|
@ -139,14 +139,10 @@ func (m *FullMessage) UpdateTimestamp(t time.Time) {
|
||||||
m.Timestamp.SetText(humanize.TimeAgoLong(t))
|
m.Timestamp.SetText(humanize.TimeAgoLong(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FullMessage) UpdateAuthor(author cchat.MessageAuthor) {
|
func (m *FullMessage) UpdateAuthor(author cchat.Author) {
|
||||||
// Call the parent's method to update the labels.
|
// Call the parent's method to update the labels.
|
||||||
m.GenericContainer.UpdateAuthor(author)
|
m.GenericContainer.UpdateAuthor(author)
|
||||||
|
m.Avatar.SetURL(author.Avatar())
|
||||||
// If the author has an avatar:
|
|
||||||
if avatarer, ok := author.(cchat.MessageAuthorAvatar); ok {
|
|
||||||
m.Avatar.SetURL(avatarer.Avatar())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyAvatarPixbuf sets the pixbuf into the given container. This shares the
|
// CopyAvatarPixbuf sets the pixbuf into the given container. This shares the
|
||||||
|
|
|
@ -192,41 +192,39 @@ func (c *GridStore) LastMessage() GridMessage {
|
||||||
|
|
||||||
// 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 *GridStore) Message(msg cchat.MessageHeader) GridMessage {
|
func (c *GridStore) Message(msgID cchat.ID, nonce string) GridMessage {
|
||||||
if m := c.message(msg); m != nil {
|
if m := c.message(msgID, nonce); m != nil {
|
||||||
return m.GridMessage
|
return m.GridMessage
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) message(msg cchat.MessageHeader) *gridMessage {
|
func (c *GridStore) message(msgID cchat.ID, nonce string) *gridMessage {
|
||||||
// Search using the ID first.
|
// Search using the ID first.
|
||||||
m, ok := c.messages[msg.ID()]
|
m, ok := c.messages[msgID]
|
||||||
if ok {
|
if ok {
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this an existing message?
|
// Is this an existing message?
|
||||||
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
if nonce != "" {
|
||||||
var nonce = noncer.Nonce()
|
|
||||||
|
|
||||||
// Things in this map are guaranteed to have presend != nil.
|
// Things in this map are guaranteed to have presend != nil.
|
||||||
m, ok := c.messages[nonce]
|
m, ok := c.messages[nonce]
|
||||||
if ok {
|
if ok {
|
||||||
// Replace the nonce key with ID.
|
// Replace the nonce key with ID.
|
||||||
delete(c.messages, nonce)
|
delete(c.messages, nonce)
|
||||||
c.messages[msg.ID()] = m
|
c.messages[msgID] = m
|
||||||
|
|
||||||
// Set the right ID.
|
// Set the right ID.
|
||||||
m.presend.SetDone(msg.ID())
|
m.presend.SetDone(msgID)
|
||||||
// Destroy the presend struct.
|
// Destroy the presend struct.
|
||||||
m.presend = nil
|
m.presend = nil
|
||||||
|
|
||||||
// Replace the nonce inside the ID slice with the actual ID.
|
// Replace the nonce inside the ID slice with the actual ID.
|
||||||
if ix := c.findIndex(nonce); ix > -1 {
|
if ix := c.findIndex(nonce); ix > -1 {
|
||||||
c.messageIDs[ix] = msg.ID()
|
c.messageIDs[ix] = msgID
|
||||||
} else {
|
} else {
|
||||||
log.Error(fmt.Errorf("Missed ID %s in slice index %d", msg.ID(), ix))
|
log.Error(fmt.Errorf("Missed ID %s in slice index %d", msgID, ix))
|
||||||
}
|
}
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
@ -280,7 +278,7 @@ func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
defer c.Controller.AuthorEvent(msg.Author())
|
defer c.Controller.AuthorEvent(msg.Author())
|
||||||
|
|
||||||
// Attempt to update before insertion (aka upsert).
|
// Attempt to update before insertion (aka upsert).
|
||||||
if msgc := c.Message(msg); msgc != nil {
|
if msgc := c.Message(msg.ID(), msg.Nonce()); msgc != nil {
|
||||||
msgc.UpdateAuthor(msg.Author())
|
msgc.UpdateAuthor(msg.Author())
|
||||||
msgc.UpdateContent(msg.Content(), false)
|
msgc.UpdateContent(msg.Content(), false)
|
||||||
msgc.UpdateTimestamp(msg.Time())
|
msgc.UpdateTimestamp(msg.Time())
|
||||||
|
@ -305,11 +303,11 @@ func (c *GridStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
|
||||||
// Call the event handler last.
|
// Call the event handler last.
|
||||||
defer c.Controller.AuthorEvent(msg.Author())
|
defer c.Controller.AuthorEvent(msg.Author())
|
||||||
|
|
||||||
if msgc := c.Message(msg); msgc != nil {
|
if msgc := c.Message(msg.ID(), ""); msgc != nil {
|
||||||
if author := msg.Author(); author != nil {
|
if author := msg.Author(); author != nil {
|
||||||
msgc.UpdateAuthor(author)
|
msgc.UpdateAuthor(author)
|
||||||
}
|
}
|
||||||
if content := msg.Content(); !content.Empty() {
|
if content := msg.Content(); !content.IsEmpty() {
|
||||||
msgc.UpdateContent(content, true)
|
msgc.UpdateContent(content, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
package completion
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/diamondburned/imgutil"
|
|
||||||
"github.com/gotk3/gotk3/gtk"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ImageSmall = 25
|
|
||||||
ImageLarge = 40
|
|
||||||
ImagePadding = 6
|
|
||||||
)
|
|
||||||
|
|
||||||
var ppIcon = []imgutil.Processor{imgutil.Round(true)}
|
|
||||||
|
|
||||||
type View struct {
|
|
||||||
*completion.Completer
|
|
||||||
entries []cchat.CompletionEntry
|
|
||||||
completer cchat.ServerMessageSendCompleter
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(text *gtk.TextView) *View {
|
|
||||||
v := &View{}
|
|
||||||
c := completion.NewCompleter(text, v)
|
|
||||||
v.Completer = c
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *View) Reset() {
|
|
||||||
v.SetCompleter(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *View) SetCompleter(completer cchat.ServerMessageSendCompleter) {
|
|
||||||
v.Clear()
|
|
||||||
v.Hide()
|
|
||||||
v.completer = completer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *View) Update(words []string, i int) []gtk.IWidget {
|
|
||||||
// If we don't have a completer, then don't run.
|
|
||||||
if v.completer == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v.entries = v.completer.CompleteMessage(words, i)
|
|
||||||
|
|
||||||
var widgets = make([]gtk.IWidget, len(v.entries))
|
|
||||||
|
|
||||||
for i, entry := range v.entries {
|
|
||||||
// Container that holds the label.
|
|
||||||
lbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
|
||||||
lbox.SetVAlign(gtk.ALIGN_CENTER)
|
|
||||||
lbox.Show()
|
|
||||||
|
|
||||||
// Label for the primary text.
|
|
||||||
l := rich.NewLabel(entry.Text)
|
|
||||||
l.Show()
|
|
||||||
lbox.PackStart(l, false, false, 0)
|
|
||||||
|
|
||||||
// Get the iamge size so we can change and use if needed. The default
|
|
||||||
var size = ImageSmall
|
|
||||||
if !entry.Secondary.Empty() {
|
|
||||||
size = ImageLarge
|
|
||||||
|
|
||||||
s := rich.NewLabel(text.Rich{})
|
|
||||||
s.SetMarkup(fmt.Sprintf(
|
|
||||||
`<span alpha="50%%" size="small">%s</span>`,
|
|
||||||
markup.Render(entry.Secondary),
|
|
||||||
))
|
|
||||||
s.Show()
|
|
||||||
|
|
||||||
lbox.PackStart(s, false, false, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
|
||||||
b.PackEnd(lbox, true, true, ImagePadding)
|
|
||||||
b.Show()
|
|
||||||
|
|
||||||
// Do we have an icon?
|
|
||||||
if entry.IconURL != "" {
|
|
||||||
img, _ := gtk.ImageNew()
|
|
||||||
img.SetMarginStart(ImagePadding)
|
|
||||||
img.SetSizeRequest(size, size)
|
|
||||||
img.Show()
|
|
||||||
|
|
||||||
// Prepend the image into the box.
|
|
||||||
b.PackEnd(img, false, false, 0)
|
|
||||||
|
|
||||||
var pps []imgutil.Processor
|
|
||||||
if !entry.Image {
|
|
||||||
pps = ppIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
httputil.AsyncImageSized(img, entry.IconURL, size, size, pps...)
|
|
||||||
}
|
|
||||||
|
|
||||||
widgets[i] = b
|
|
||||||
}
|
|
||||||
|
|
||||||
return widgets
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *View) Word(i int) string {
|
|
||||||
return v.entries[i].Raw
|
|
||||||
}
|
|
|
@ -7,9 +7,9 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/attachment"
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/attachment"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username"
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||||
"github.com/diamondburned/gspell"
|
"github.com/diamondburned/gspell"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
@ -24,7 +24,7 @@ type Controller interface {
|
||||||
|
|
||||||
type InputView struct {
|
type InputView struct {
|
||||||
*Field
|
*Field
|
||||||
Completer *completion.View
|
Completer *completion.Completer
|
||||||
}
|
}
|
||||||
|
|
||||||
var textCSS = primitives.PrepareCSS(`
|
var textCSS = primitives.PrepareCSS(`
|
||||||
|
@ -69,7 +69,7 @@ func NewView(ctrl Controller) *InputView {
|
||||||
primitives.AttachCSS(text, textCSS)
|
primitives.AttachCSS(text, textCSS)
|
||||||
|
|
||||||
// Bind the text event handler to text first.
|
// Bind the text event handler to text first.
|
||||||
c := completion.New(text)
|
c := completion.NewCompleter(text)
|
||||||
|
|
||||||
// Bind the input callback later.
|
// Bind the input callback later.
|
||||||
f := NewField(text, ctrl)
|
f := NewField(text, ctrl)
|
||||||
|
@ -78,12 +78,18 @@ func NewView(ctrl Controller) *InputView {
|
||||||
return &InputView{f, c}
|
return &InputView{f, c}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
func (v *InputView) SetMessenger(session cchat.Session, messenger cchat.Messenger) {
|
||||||
v.Field.SetSender(session, sender)
|
v.Field.SetMessenger(session, messenger)
|
||||||
|
|
||||||
|
if messenger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore ok; completer can be nil.
|
// Ignore ok; completer can be nil.
|
||||||
completer, _ := sender.(cchat.ServerMessageSendCompleter)
|
// TODO: this is possibly racy vs the above SetMessenger.
|
||||||
v.Completer.SetCompleter(completer)
|
if sender := messenger.AsSender(); sender != nil {
|
||||||
|
v.Completer.SetCompleter(sender.AsCompleter())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
|
@ -111,11 +117,12 @@ type Field struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type fieldState struct {
|
type fieldState struct {
|
||||||
UserID string
|
UserID string
|
||||||
Sender cchat.ServerMessageSender
|
Messenger cchat.Messenger
|
||||||
upload bool // true if server supports files
|
Sender cchat.Sender
|
||||||
editor cchat.ServerMessageEditor
|
upload bool // true if server supports files
|
||||||
typer cchat.ServerMessageTypingIndicator
|
editor cchat.Editor
|
||||||
|
typing cchat.TypingIndicator
|
||||||
|
|
||||||
editingID string // never empty
|
editingID string // never empty
|
||||||
lastTyped time.Time
|
lastTyped time.Time
|
||||||
|
@ -215,30 +222,30 @@ func (f *Field) Reset() {
|
||||||
f.clearText()
|
f.clearText()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSender changes the sender of the input field. If nil, the input will be
|
// SetMessenger changes the messenger of the input field. If nil, the input
|
||||||
// disabled. Reset() should be called first.
|
// will be disabled. Reset() should be called first.
|
||||||
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
func (f *Field) SetMessenger(session cchat.Session, messenger cchat.Messenger) {
|
||||||
// Update the left username container in the input.
|
// Update the left username container in the input.
|
||||||
f.Username.Update(session, sender)
|
f.Username.Update(session, messenger)
|
||||||
f.UserID = session.ID()
|
f.UserID = session.ID()
|
||||||
|
|
||||||
// Set the sender.
|
// Set the sender.
|
||||||
if sender != nil {
|
if messenger != nil {
|
||||||
f.Sender = sender
|
f.Messenger = messenger
|
||||||
|
f.Sender = messenger.AsSender()
|
||||||
f.text.SetSensitive(true)
|
f.text.SetSensitive(true)
|
||||||
|
|
||||||
// Allow editor to be nil.
|
// Allow editor to be nil.
|
||||||
f.editor, _ = sender.(cchat.ServerMessageEditor)
|
f.editor = f.Messenger.AsEditor()
|
||||||
// Allow typer to be nil.
|
// Allow typer to be nil.
|
||||||
f.typer, _ = sender.(cchat.ServerMessageTypingIndicator)
|
f.typing = f.Messenger.AsTypingIndicator()
|
||||||
|
|
||||||
// See if we can upload files.
|
// See if we can upload files.
|
||||||
_, allowUpload := sender.(cchat.ServerMessageAttachmentSender)
|
f.SetAllowUpload(f.Sender.CanAttach())
|
||||||
f.SetAllowUpload(allowUpload)
|
|
||||||
|
|
||||||
// Populate the duration state if typer is not nil.
|
// Populate the duration state if typer is not nil.
|
||||||
if f.typer != nil {
|
if f.typing != nil {
|
||||||
f.typerDura = f.typer.TypingTimeout()
|
f.typerDura = f.typing.TypingTimeout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,7 +269,7 @@ func (f *Field) SetAllowUpload(allow bool) {
|
||||||
|
|
||||||
// Editable returns whether or not the input field can be edited.
|
// Editable returns whether or not the input field can be edited.
|
||||||
func (f *Field) Editable(msgID string) bool {
|
func (f *Field) Editable(msgID string) bool {
|
||||||
return f.editor != nil && f.editor.MessageEditable(msgID)
|
return f.editor != nil && f.editor.IsEditable(msgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) StartEditing(msgID string) bool {
|
func (f *Field) StartEditing(msgID string) bool {
|
||||||
|
@ -272,7 +279,7 @@ func (f *Field) StartEditing(msgID string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try and request the old message content for editing.
|
// Try and request the old message content for editing.
|
||||||
content, err := f.editor.RawMessageContent(msgID)
|
content, err := f.editor.RawContent(msgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: show error
|
// TODO: show error
|
||||||
log.Error(errors.Wrap(err, "Failed to get message content"))
|
log.Error(errors.Wrap(err, "Failed to get message content"))
|
||||||
|
|
|
@ -105,7 +105,7 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
|
||||||
|
|
||||||
// If the server supports typing indication, then announce that we are
|
// If the server supports typing indication, then announce that we are
|
||||||
// typing with a proper rate limit.
|
// typing with a proper rate limit.
|
||||||
if f.typer != nil {
|
if f.typing != nil {
|
||||||
// Get the current time; if the next timestamp is before now, then that
|
// Get the current time; if the next timestamp is before now, then that
|
||||||
// means it's time for us to update it and send a typing indication.
|
// means it's time for us to update it and send a typing indication.
|
||||||
if now := time.Now(); f.lastTyped.Add(f.typerDura).Before(now) {
|
if now := time.Now(); f.lastTyped.Add(f.typerDura).Before(now) {
|
||||||
|
@ -113,7 +113,7 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
|
||||||
f.lastTyped = now
|
f.lastTyped = now
|
||||||
// Send asynchronously.
|
// Send asynchronously.
|
||||||
go func() {
|
go func() {
|
||||||
if err := f.typer.Typing(); err != nil {
|
if err := f.typing.Typing(); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Failed to announce typing"))
|
log.Error(errors.Wrap(err, "Failed to announce typing"))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -46,7 +46,7 @@ func (f *Field) sendInput() {
|
||||||
// Are we editing anything?
|
// Are we editing anything?
|
||||||
if id := f.editingID; f.Editable(id) && id != "" {
|
if id := f.editingID; f.Editable(id) && id != "" {
|
||||||
go func() {
|
go func() {
|
||||||
if err := f.editor.EditMessage(id, text); err != nil {
|
if err := f.editor.Edit(id, text); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Failed to edit message"))
|
log.Error(errors.Wrap(err, "Failed to edit message"))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -87,7 +87,7 @@ func (f *Field) SendMessage(data PresendMessage) {
|
||||||
// Copy the sender to prevent race conditions.
|
// Copy the sender to prevent race conditions.
|
||||||
var sender = f.Sender
|
var sender = f.Sender
|
||||||
gts.Async(func() (func(), error) {
|
gts.Async(func() (func(), error) {
|
||||||
if err := sender.SendMessage(data); err != nil {
|
if err := sender.Send(data); err != nil {
|
||||||
return func() { onErr(err) }, errors.Wrap(err, "Failed to send message")
|
return func() { onErr(err) }, errors.Wrap(err, "Failed to send message")
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -104,11 +104,13 @@ type SendMessageData struct {
|
||||||
files []attachment.File
|
files []attachment.File
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ cchat.SendableMessage = (*SendMessageData)(nil)
|
||||||
|
|
||||||
type PresendMessage interface {
|
type PresendMessage interface {
|
||||||
cchat.MessageHeader // returns nonce and time
|
cchat.MessageHeader // returns nonce and time
|
||||||
cchat.SendableMessage
|
cchat.SendableMessage
|
||||||
cchat.MessageNonce
|
cchat.Noncer
|
||||||
cchat.SendableMessageAttachments
|
cchat.Attachments
|
||||||
|
|
||||||
// These methods are reserved for internal use.
|
// These methods are reserved for internal use.
|
||||||
|
|
||||||
|
@ -145,6 +147,10 @@ func (s SendMessageData) AuthorAvatarURL() string {
|
||||||
return s.authorURL
|
return s.authorURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SendMessageData) AsNoncer() cchat.Noncer {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (s SendMessageData) Nonce() string {
|
func (s SendMessageData) Nonce() string {
|
||||||
return s.nonce
|
return s.nonce
|
||||||
}
|
}
|
||||||
|
@ -153,6 +159,10 @@ func (s SendMessageData) Files() []attachment.File {
|
||||||
return s.files
|
return s.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SendMessageData) AsAttachments() cchat.Attachments {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (s SendMessageData) Attachments() []cchat.MessageAttachment {
|
func (s SendMessageData) Attachments() []cchat.MessageAttachment {
|
||||||
var attachments = make([]cchat.MessageAttachment, len(s.files))
|
var attachments = make([]cchat.MessageAttachment, len(s.files))
|
||||||
for i, file := range s.files {
|
for i, file := range s.files {
|
||||||
|
|
|
@ -82,7 +82,7 @@ func (u *Container) SetRevealChild(reveal bool) {
|
||||||
|
|
||||||
// shouldReveal returns whether or not the container should reveal.
|
// shouldReveal returns whether or not the container should reveal.
|
||||||
func (u *Container) shouldReveal() bool {
|
func (u *Container) shouldReveal() bool {
|
||||||
return (!u.label.GetLabel().Empty() || u.avatar.URL() != "") && showUser
|
return (!u.label.GetLabel().IsEmpty() || u.avatar.URL() != "") && showUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Container) Reset() {
|
func (u *Container) Reset() {
|
||||||
|
@ -92,19 +92,19 @@ func (u *Container) Reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update is not thread-safe.
|
// Update is not thread-safe.
|
||||||
func (u *Container) Update(session cchat.Session, sender cchat.ServerMessageSender) {
|
func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) {
|
||||||
// Set the fallback username.
|
// Set the fallback username.
|
||||||
u.label.SetLabelUnsafe(session.Name())
|
u.label.SetLabelUnsafe(session.Name())
|
||||||
// Reveal the name if it's not empty.
|
// Reveal the name if it's not empty.
|
||||||
u.SetRevealChild(true)
|
u.SetRevealChild(true)
|
||||||
|
|
||||||
// Does sender (aka Server) implement ServerNickname? If yes, use it.
|
// Does messenger implement Nicknamer? If yes, use it.
|
||||||
if nicknamer, ok := sender.(cchat.ServerNickname); ok {
|
if nicknamer := messenger.AsNicknamer(); nicknamer != nil {
|
||||||
u.label.AsyncSetLabel(nicknamer.Nickname, "Error fetching server nickname")
|
u.label.AsyncSetLabel(nicknamer.Nickname, "Error fetching server nickname")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does session implement an icon? Update if yes.
|
// Does session implement an icon? Update if yes.
|
||||||
if iconer, ok := session.(cchat.Icon); ok {
|
if iconer := session.AsIconer(); iconer != nil {
|
||||||
u.avatar.AsyncSetIconer(iconer, "Error fetching session icon URL")
|
u.avatar.AsyncSetIconer(iconer, "Error fetching session icon URL")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,9 +93,9 @@ func (c *Container) Reset() {
|
||||||
|
|
||||||
// TryAsyncList tries to set the member list from the given server. It does type
|
// TryAsyncList tries to set the member list from the given server. It does type
|
||||||
// assertions and handles asynchronicity. Reset must be called before this.
|
// assertions and handles asynchronicity. Reset must be called before this.
|
||||||
func (c *Container) TryAsyncList(server cchat.ServerMessage) {
|
func (c *Container) TryAsyncList(server cchat.Messenger) {
|
||||||
ls, ok := server.(cchat.ServerMessageMemberLister)
|
ls := server.AsMemberLister()
|
||||||
if !ok {
|
if ls == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ func (c *Container) TryAsyncList(server cchat.ServerMessage) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) SetSections(sections []cchat.MemberListSection) {
|
func (c *Container) SetSections(sections []cchat.MemberSection) {
|
||||||
gts.ExecAsync(func() { c.SetSectionsUnsafe(sections) })
|
gts.ExecAsync(func() { c.SetSectionsUnsafe(sections) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ func (c *Container) RemoveMember(sectionID string, id string) {
|
||||||
gts.ExecAsync(func() { c.RemoveMemberUnsafe(sectionID, id) })
|
gts.ExecAsync(func() { c.RemoveMemberUnsafe(sectionID, id) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) SetSectionsUnsafe(sections []cchat.MemberListSection) {
|
func (c *Container) SetSectionsUnsafe(sections []cchat.MemberSection) {
|
||||||
var newSections = make([]*Section, len(sections))
|
var newSections = make([]*Section, len(sections))
|
||||||
|
|
||||||
for i, section := range sections {
|
for i, section := range sections {
|
||||||
|
@ -191,7 +191,7 @@ var sectionBodyCSS = primitives.PrepareClassCSS("section-body", `
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
func NewSection(sect cchat.MemberListSection) *Section {
|
func NewSection(sect cchat.MemberSection) *Section {
|
||||||
header := rich.NewLabel(text.Rich{})
|
header := rich.NewLabel(text.Rich{})
|
||||||
header.Show()
|
header.Show()
|
||||||
sectionHeaderCSS(header)
|
sectionHeaderCSS(header)
|
||||||
|
@ -359,7 +359,7 @@ func NewMember(member cchat.ListMember) *Member {
|
||||||
func (m *Member) Update(member cchat.ListMember) {
|
func (m *Member) Update(member cchat.ListMember) {
|
||||||
m.ListBoxRow.SetName(member.Name().Content)
|
m.ListBoxRow.SetName(member.Name().Content)
|
||||||
|
|
||||||
if iconer, ok := member.(cchat.Icon); ok {
|
if iconer := member.AsIconer(); iconer != nil {
|
||||||
m.Avatar.AsyncSetIconer(iconer, "Failed to get member list icon")
|
m.Avatar.AsyncSetIconer(iconer, "Failed to get member list icon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,7 +370,7 @@ func (m *Member) Update(member cchat.ListMember) {
|
||||||
statusColors(member.Status()), m.output.Markup,
|
statusColors(member.Status()), m.output.Markup,
|
||||||
))
|
))
|
||||||
|
|
||||||
if bot := member.Secondary(); !bot.Empty() {
|
if bot := member.Secondary(); !bot.IsEmpty() {
|
||||||
txt.WriteByte('\n')
|
txt.WriteByte('\n')
|
||||||
txt.WriteString(fmt.Sprintf(
|
txt.WriteString(fmt.Sprintf(
|
||||||
`<span alpha="85%%"><sup>%s</sup></span>`,
|
`<span alpha="85%%"><sup>%s</sup></span>`,
|
||||||
|
@ -392,15 +392,15 @@ func (m *Member) Popup() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusColors(status cchat.UserStatus) uint32 {
|
func statusColors(status cchat.Status) uint32 {
|
||||||
switch status {
|
switch status {
|
||||||
case cchat.OnlineStatus:
|
case cchat.StatusOnline:
|
||||||
return 0x43B581
|
return 0x43B581
|
||||||
case cchat.BusyStatus:
|
case cchat.StatusBusy:
|
||||||
return 0xF04747
|
return 0xF04747
|
||||||
case cchat.IdleStatus:
|
case cchat.StatusIdle:
|
||||||
return 0xFAA61A
|
return 0xFAA61A
|
||||||
case cchat.OfflineStatus:
|
case cchat.StatusOffline:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
return 0x747F8D
|
return 0x747F8D
|
||||||
|
|
|
@ -21,7 +21,7 @@ type Container interface {
|
||||||
AvatarURL() string // avatar
|
AvatarURL() string // avatar
|
||||||
Nonce() string
|
Nonce() string
|
||||||
|
|
||||||
UpdateAuthor(cchat.MessageAuthor)
|
UpdateAuthor(cchat.Author)
|
||||||
UpdateAuthorName(text.Rich)
|
UpdateAuthorName(text.Rich)
|
||||||
UpdateContent(c text.Rich, edited bool)
|
UpdateContent(c text.Rich, edited bool)
|
||||||
UpdateTimestamp(time.Time)
|
UpdateTimestamp(time.Time)
|
||||||
|
@ -79,12 +79,9 @@ func NewContainer(msg cchat.MessageCreate) *GenericContainer {
|
||||||
c := NewEmptyContainer()
|
c := NewEmptyContainer()
|
||||||
c.id = msg.ID()
|
c.id = msg.ID()
|
||||||
c.time = msg.Time()
|
c.time = msg.Time()
|
||||||
|
c.nonce = msg.Nonce()
|
||||||
c.authorID = msg.Author().ID()
|
c.authorID = msg.Author().ID()
|
||||||
|
|
||||||
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
|
||||||
c.nonce = noncer.Nonce()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,14 +177,10 @@ func (m *GenericContainer) UpdateTimestamp(t time.Time) {
|
||||||
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
|
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) UpdateAuthor(author cchat.MessageAuthor) {
|
func (m *GenericContainer) UpdateAuthor(author cchat.Author) {
|
||||||
m.authorID = author.ID()
|
m.authorID = author.ID()
|
||||||
|
m.avatarURL = author.Avatar()
|
||||||
m.UpdateAuthorName(author.Name())
|
m.UpdateAuthorName(author.Name())
|
||||||
|
|
||||||
// Set the avatar URL for future access on-demand.
|
|
||||||
if avatarer, ok := author.(cchat.MessageAuthorAvatar); ok {
|
|
||||||
m.avatarURL = avatarer.Avatar()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
|
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ type State struct {
|
||||||
stopper func() // stops the event loop, not used atm
|
stopper func() // stops the event loop, not used atm
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cchat.TypingIndicator = (*State)(nil)
|
var _ cchat.TypingContainer = (*State)(nil)
|
||||||
|
|
||||||
func NewState(changed func(s *State, empty bool)) *State {
|
func NewState(changed func(s *State, empty bool)) *State {
|
||||||
s := &State{changed: changed}
|
s := &State{changed: changed}
|
||||||
|
@ -41,7 +41,7 @@ func (s *State) reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe is thread-safe.
|
// Subscribe is thread-safe.
|
||||||
func (s *State) Subscribe(indicator cchat.ServerMessageTypingIndicator) {
|
func (s *State) Subscribe(indicator cchat.TypingIndicator) {
|
||||||
gts.Async(func() (func(), error) {
|
gts.Async(func() (func(), error) {
|
||||||
c, err := indicator.TypingSubscribe(s)
|
c, err := indicator.TypingSubscribe(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -71,17 +71,17 @@ func (c *Container) Reset() {
|
||||||
c.SetRevealChild(false)
|
c.SetRevealChild(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) RemoveAuthor(author cchat.MessageAuthor) {
|
func (c *Container) RemoveAuthor(author cchat.Author) {
|
||||||
c.state.removeTyper(author.ID())
|
c.state.removeTyper(author.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) TrySubscribe(svmsg cchat.ServerMessage) bool {
|
func (c *Container) TrySubscribe(svmsg cchat.Messenger) bool {
|
||||||
ti, ok := svmsg.(cchat.ServerMessageTypingIndicator)
|
var tindicator = svmsg.AsTypingIndicator()
|
||||||
if !ok {
|
if tindicator == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
c.state.Subscribe(ti)
|
c.state.Subscribe(tindicator)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -260,7 +260,7 @@ func (v *View) MemberListUpdated(c *memberlist.Container) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
|
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
|
||||||
func (v *View) JoinServer(session cchat.Session, server ServerMessage, bc traverse.Breadcrumber) {
|
func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) {
|
||||||
// Reset before setting.
|
// Reset before setting.
|
||||||
v.Reset()
|
v.Reset()
|
||||||
|
|
||||||
|
@ -268,21 +268,25 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, bc traver
|
||||||
v.FaceView.SetLoading()
|
v.FaceView.SetLoading()
|
||||||
v.ctrl.OnMessageBusy()
|
v.ctrl.OnMessageBusy()
|
||||||
|
|
||||||
// Bind the state.
|
// Get the messenger once.
|
||||||
v.state.bind(session, server)
|
var messenger = server.AsMessenger()
|
||||||
|
// Exit if this server is not a messenger.
|
||||||
|
if messenger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind the state.
|
||||||
|
v.state.bind(session, server, messenger)
|
||||||
|
|
||||||
// Skipping ok check because sender can be nil. Without the empty
|
|
||||||
// check, Go will panic.
|
|
||||||
sender, _ := server.(cchat.ServerMessageSender)
|
|
||||||
// We're setting this variable before actually calling JoinServer. This is
|
// We're setting this variable before actually calling JoinServer. This is
|
||||||
// because new messages created by JoinServer will use this state for things
|
// because new messages created by JoinServer will use this state for things
|
||||||
// such as determinining if it's deletable or not.
|
// such as determinining if it's deletable or not.
|
||||||
v.InputView.SetSender(session, sender)
|
v.InputView.SetMessenger(session, messenger)
|
||||||
|
|
||||||
gts.Async(func() (func(), error) {
|
gts.Async(func() (func(), error) {
|
||||||
// We can use a background context here, as the user can't go anywhere
|
// We can use a background context here, as the user can't go anywhere
|
||||||
// that would require cancellation anyway. This is done in ui.go.
|
// that would require cancellation anyway. This is done in ui.go.
|
||||||
s, err := server.JoinServer(context.Background(), v.Container)
|
s, err := messenger.JoinServer(context.Background(), v.Container)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Wrap(err, "Failed to join server")
|
err = errors.Wrap(err, "Failed to join server")
|
||||||
// Even if we're erroring out, we're running the done() callback
|
// Even if we're erroring out, we're running the done() callback
|
||||||
|
@ -304,10 +308,10 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, bc traver
|
||||||
v.Header.SetBreadcrumber(bc)
|
v.Header.SetBreadcrumber(bc)
|
||||||
|
|
||||||
// Try setting the typing indicator if available.
|
// Try setting the typing indicator if available.
|
||||||
v.Typing.TrySubscribe(server)
|
v.Typing.TrySubscribe(messenger)
|
||||||
|
|
||||||
// Try and use the list.
|
// Try and use the list.
|
||||||
v.MemberList.TryAsyncList(server)
|
v.MemberList.TryAsyncList(messenger)
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -338,7 +342,7 @@ func (v *View) FetchBacklog() {
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err := backlogger.MessagesBefore(ctx, firstMsg.ID(), v.Container)
|
err := backlogger.Backlog(ctx, firstMsg.ID(), v.Container)
|
||||||
return done, errors.Wrap(err, "Failed to get messages before ID")
|
return done, errors.Wrap(err, "Failed to get messages before ID")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -361,7 +365,7 @@ func (v *View) AddPresendMessage(msg input.PresendMessage) func(error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorEvent should be called on message create/update/delete.
|
// AuthorEvent should be called on message create/update/delete.
|
||||||
func (v *View) AuthorEvent(author cchat.MessageAuthor) {
|
func (v *View) AuthorEvent(author cchat.Author) {
|
||||||
// Remove the author from the typing list if it's not nil.
|
// Remove the author from the typing list if it's not nil.
|
||||||
if author != nil {
|
if author != nil {
|
||||||
v.Typing.RemoveAuthor(author)
|
v.Typing.RemoveAuthor(author)
|
||||||
|
@ -381,7 +385,7 @@ func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendG
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := sender.SendMessage(msg); err != nil {
|
if err := sender.Send(msg); err != nil {
|
||||||
// Set the message's state to errored again, but we don't need to
|
// Set the message's state to errored again, but we don't need to
|
||||||
// rebind the menu.
|
// rebind the menu.
|
||||||
gts.ExecAsync(func() { presend.SetSentError(err) })
|
gts.ExecAsync(func() { presend.SetSentError(err) })
|
||||||
|
@ -404,7 +408,7 @@ func (v *View) BindMenu(msg container.GridMessage) {
|
||||||
|
|
||||||
// Do we have any custom actions? If yes, append it.
|
// Do we have any custom actions? If yes, append it.
|
||||||
if v.hasActions() {
|
if v.hasActions() {
|
||||||
var actions = v.actioner.MessageActions(msg.ID())
|
var actions = v.actioner.Actions(msg.ID())
|
||||||
var items = make([]menu.Item, len(actions))
|
var items = make([]menu.Item, len(actions))
|
||||||
|
|
||||||
for i, action := range actions {
|
for i, action := range actions {
|
||||||
|
@ -424,7 +428,7 @@ func (v *View) makeActionItem(action, msgID string) menu.Item {
|
||||||
go func() {
|
go func() {
|
||||||
// Run, get the error, and try to log it. The logger will ignore nil
|
// Run, get the error, and try to log it. The logger will ignore nil
|
||||||
// errors.
|
// errors.
|
||||||
err := v.state.actioner.DoMessageAction(action, msgID)
|
err := v.state.actioner.Do(action, msgID)
|
||||||
log.Error(errors.Wrap(err, "Failed to do action "+action))
|
log.Error(errors.Wrap(err, "Failed to do action "+action))
|
||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
@ -433,15 +437,15 @@ func (v *View) makeActionItem(action, msgID string) menu.Item {
|
||||||
// ServerMessage combines Server and ServerMessage from cchat.
|
// ServerMessage combines Server and ServerMessage from cchat.
|
||||||
type ServerMessage interface {
|
type ServerMessage interface {
|
||||||
cchat.Server
|
cchat.Server
|
||||||
cchat.ServerMessage
|
cchat.Messenger
|
||||||
}
|
}
|
||||||
|
|
||||||
type state struct {
|
type state struct {
|
||||||
session cchat.Session
|
session cchat.Session
|
||||||
server cchat.Server
|
server cchat.Server
|
||||||
|
|
||||||
actioner cchat.ServerMessageActioner
|
actioner cchat.Actioner
|
||||||
backlogger cchat.ServerMessageBacklogger
|
backlogger cchat.Backlogger
|
||||||
|
|
||||||
current func() // stop callback
|
current func() // stop callback
|
||||||
author string
|
author string
|
||||||
|
@ -483,7 +487,7 @@ const backloggingFreq = time.Second * 3
|
||||||
|
|
||||||
// Backlogger returns the backlogger instance if it's allowed to fetch more
|
// Backlogger returns the backlogger instance if it's allowed to fetch more
|
||||||
// backlogs.
|
// backlogs.
|
||||||
func (s *state) Backlogger() cchat.ServerMessageBacklogger {
|
func (s *state) Backlogger() cchat.Backlogger {
|
||||||
if s.backlogger == nil || s.current == nil {
|
if s.backlogger == nil || s.current == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -498,11 +502,11 @@ func (s *state) Backlogger() cchat.ServerMessageBacklogger {
|
||||||
return s.backlogger
|
return s.backlogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) bind(session cchat.Session, server ServerMessage) {
|
func (s *state) bind(session cchat.Session, server cchat.Server, msgr cchat.Messenger) {
|
||||||
s.session = session
|
s.session = session
|
||||||
s.server = server
|
s.server = server
|
||||||
s.actioner, _ = server.(cchat.ServerMessageActioner)
|
s.actioner = msgr.AsActioner()
|
||||||
s.backlogger, _ = server.(cchat.ServerMessageBacklogger)
|
s.backlogger = msgr.AsBacklogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) setcurrent(fn func()) {
|
func (s *state) setcurrent(fn func()) {
|
||||||
|
|
|
@ -1,34 +1,51 @@
|
||||||
package completion
|
package completion
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/diamondburned/cchat/utils/split"
|
"github.com/diamondburned/cchat/utils/split"
|
||||||
|
"github.com/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Completeable interface {
|
const (
|
||||||
Update([]string, int) []gtk.IWidget
|
ImageSmall = 25
|
||||||
Word(i int) string
|
ImageLarge = 40
|
||||||
}
|
ImagePadding = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// post-processor icon
|
||||||
|
var ppIcon = []imgutil.Processor{imgutil.Round(true)}
|
||||||
|
|
||||||
type Completer struct {
|
type Completer struct {
|
||||||
ctrl Completeable
|
|
||||||
|
|
||||||
Input *gtk.TextView
|
Input *gtk.TextView
|
||||||
Buffer *gtk.TextBuffer
|
Buffer *gtk.TextBuffer
|
||||||
List *gtk.ListBox
|
List *gtk.ListBox
|
||||||
Popover *gtk.Popover
|
Popover *gtk.Popover
|
||||||
|
popdown bool
|
||||||
|
|
||||||
Words []string
|
Splitter split.SplitFunc
|
||||||
Index int
|
|
||||||
Cursor int
|
words []string
|
||||||
|
index int64
|
||||||
|
cursor int64
|
||||||
|
|
||||||
|
entries []cchat.CompletionEntry
|
||||||
|
completer cchat.Completer
|
||||||
}
|
}
|
||||||
|
|
||||||
func WrapCompleter(input *gtk.TextView, ctrl Completeable) {
|
func WrapCompleter(input *gtk.TextView) {
|
||||||
NewCompleter(input, ctrl)
|
NewCompleter(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
|
func NewCompleter(input *gtk.TextView) *Completer {
|
||||||
l, _ := gtk.ListBoxNew()
|
l, _ := gtk.ListBoxNew()
|
||||||
l.Show()
|
l.Show()
|
||||||
|
|
||||||
|
@ -43,11 +60,11 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
|
||||||
ibuf, _ := input.GetBuffer()
|
ibuf, _ := input.GetBuffer()
|
||||||
|
|
||||||
c := &Completer{
|
c := &Completer{
|
||||||
Input: input,
|
Input: input,
|
||||||
Buffer: ibuf,
|
Buffer: ibuf,
|
||||||
List: l,
|
List: l,
|
||||||
Popover: p,
|
Popover: p,
|
||||||
ctrl: ctrl,
|
Splitter: split.SpaceIndexed,
|
||||||
}
|
}
|
||||||
|
|
||||||
// This one is for buffer modification.
|
// This one is for buffer modification.
|
||||||
|
@ -56,17 +73,40 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
|
||||||
input.Connect("move-cursor", c.onChange)
|
input.Connect("move-cursor", c.onChange)
|
||||||
|
|
||||||
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||||
SwapWord(ibuf, ctrl.Word(r.GetIndex()), c.Cursor)
|
SwapWord(ibuf, c.entries[r.GetIndex()].Raw, c.cursor)
|
||||||
|
c.onChange() // signal change
|
||||||
c.Clear()
|
c.Clear()
|
||||||
c.Hide()
|
c.Popdown()
|
||||||
input.GrabFocus()
|
input.GrabFocus()
|
||||||
})
|
})
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Completer) Hide() {
|
// SetCompleter sets the current completer. If completer is nil, then the
|
||||||
c.Popover.Popdown()
|
// completer is disabled.
|
||||||
|
func (c *Completer) SetCompleter(completer cchat.Completer) {
|
||||||
|
c.Clear()
|
||||||
|
c.Popdown()
|
||||||
|
c.completer = completer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Completer) Reset() {
|
||||||
|
c.SetCompleter(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Completer) Popup() {
|
||||||
|
if c.popdown {
|
||||||
|
c.Popover.Popup()
|
||||||
|
c.popdown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Completer) Popdown() {
|
||||||
|
if !c.popdown {
|
||||||
|
c.Popover.Popdown()
|
||||||
|
c.popdown = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Completer) Clear() {
|
func (c *Completer) Clear() {
|
||||||
|
@ -82,19 +122,36 @@ func (c *Completer) Clear() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Words returns the buffer content split into words.
|
||||||
|
func (c *Completer) Content() []string {
|
||||||
|
// This method not to be confused with c.words, which contains the state of
|
||||||
|
// completer words.
|
||||||
|
|
||||||
|
text, _ := c.Buffer.GetText(c.Buffer.GetStartIter(), c.Buffer.GetEndIter(), true)
|
||||||
|
if text == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
words, _ := c.Splitter(text, 0)
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Completer) onChange() {
|
func (c *Completer) onChange() {
|
||||||
t, v, blank := State(c.Buffer)
|
t, v, blank := State(c.Buffer)
|
||||||
c.Cursor = v
|
c.cursor = v
|
||||||
|
|
||||||
// If the curssor is on a blank character, then we should not
|
log.Println("STATE:", t, v, blank)
|
||||||
|
|
||||||
|
// If the cursor is on a blank character, then we should not
|
||||||
// autocomplete anything, so we set the states to nil.
|
// autocomplete anything, so we set the states to nil.
|
||||||
if blank {
|
if blank {
|
||||||
c.Words = nil
|
c.words = nil
|
||||||
c.Index = -1
|
c.index = -1
|
||||||
} else {
|
log.Println("RESET INDEX TO -1")
|
||||||
c.Words, c.Index = split.SpaceIndexed(t, v)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.words, c.index = c.Splitter(t, v)
|
||||||
|
log.Println("INDEX:", c.index)
|
||||||
c.complete()
|
c.complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,15 +159,15 @@ func (c *Completer) complete() {
|
||||||
c.Clear()
|
c.Clear()
|
||||||
|
|
||||||
var widgets []gtk.IWidget
|
var widgets []gtk.IWidget
|
||||||
if len(c.Words) > 0 {
|
if len(c.words) > 0 {
|
||||||
widgets = c.ctrl.Update(c.Words, c.Index)
|
widgets = c.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(widgets) > 0 {
|
if len(widgets) > 0 {
|
||||||
c.Popover.SetPointingTo(CursorRect(c.Input))
|
c.Popover.SetPointingTo(CursorRect(c.Input))
|
||||||
c.Popover.Popup()
|
c.Popup()
|
||||||
} else {
|
} else {
|
||||||
c.Hide()
|
c.Popdown()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,3 +183,67 @@ func (c *Completer) complete() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Completer) update() []gtk.IWidget {
|
||||||
|
// If we don't have a completer, then don't run.
|
||||||
|
if c.completer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.entries = c.completer.Complete(c.words, c.index)
|
||||||
|
|
||||||
|
var widgets = make([]gtk.IWidget, len(c.entries))
|
||||||
|
|
||||||
|
for i, entry := range c.entries {
|
||||||
|
// Container that holds the label.
|
||||||
|
lbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
lbox.SetVAlign(gtk.ALIGN_CENTER)
|
||||||
|
lbox.Show()
|
||||||
|
|
||||||
|
// Label for the primary text.
|
||||||
|
l := rich.NewLabel(entry.Text)
|
||||||
|
l.Show()
|
||||||
|
lbox.PackStart(l, false, false, 0)
|
||||||
|
|
||||||
|
// Get the iamge size so we can change and use if needed. The default
|
||||||
|
var size = ImageSmall
|
||||||
|
if !entry.Secondary.IsEmpty() {
|
||||||
|
size = ImageLarge
|
||||||
|
|
||||||
|
s := rich.NewLabel(text.Rich{})
|
||||||
|
s.SetMarkup(fmt.Sprintf(
|
||||||
|
`<span alpha="50%%" size="small">%s</span>`,
|
||||||
|
markup.Render(entry.Secondary),
|
||||||
|
))
|
||||||
|
s.Show()
|
||||||
|
|
||||||
|
lbox.PackStart(s, false, false, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
b.PackEnd(lbox, true, true, ImagePadding)
|
||||||
|
b.Show()
|
||||||
|
|
||||||
|
// Do we have an icon?
|
||||||
|
if entry.IconURL != "" {
|
||||||
|
img, _ := gtk.ImageNew()
|
||||||
|
img.SetMarginStart(ImagePadding)
|
||||||
|
img.SetSizeRequest(size, size)
|
||||||
|
img.Show()
|
||||||
|
|
||||||
|
// Prepend the image into the box.
|
||||||
|
b.PackEnd(img, false, false, 0)
|
||||||
|
|
||||||
|
var pps []imgutil.Processor
|
||||||
|
if !entry.Image {
|
||||||
|
pps = ppIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
httputil.AsyncImageSized(img, entry.IconURL, size, size, pps...)
|
||||||
|
}
|
||||||
|
|
||||||
|
widgets[i] = b
|
||||||
|
}
|
||||||
|
|
||||||
|
return widgets
|
||||||
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ func KeyDownHandler(l *gtk.ListBox, focus func()) KeyDownHandlerFn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SwapWord(b *gtk.TextBuffer, word string, offset int) {
|
func SwapWord(b *gtk.TextBuffer, word string, offset int64) {
|
||||||
// Get iter for word replacing.
|
// Get iter for word replacing.
|
||||||
start, end := GetWordIters(b, offset)
|
start, end := GetWordIters(b, offset)
|
||||||
b.Delete(start, end)
|
b.Delete(start, end)
|
||||||
|
@ -93,7 +93,7 @@ func CursorRect(i *gtk.TextView) gdk.Rectangle {
|
||||||
return *r
|
return *r
|
||||||
}
|
}
|
||||||
|
|
||||||
func State(buf *gtk.TextBuffer) (text string, offset int, blank bool) {
|
func State(buf *gtk.TextBuffer) (text string, offset int64, blank bool) {
|
||||||
// obtain current state
|
// obtain current state
|
||||||
mark := buf.GetInsert()
|
mark := buf.GetInsert()
|
||||||
iter := buf.GetIterAtMark(mark)
|
iter := buf.GetIterAtMark(mark)
|
||||||
|
@ -102,7 +102,7 @@ func State(buf *gtk.TextBuffer) (text string, offset int, blank bool) {
|
||||||
start, end := buf.GetBounds()
|
start, end := buf.GetBounds()
|
||||||
|
|
||||||
text, _ = buf.GetText(start, end, true)
|
text, _ = buf.GetText(start, end, true)
|
||||||
offset = iter.GetOffset()
|
offset = int64(iter.GetOffset())
|
||||||
|
|
||||||
// We need the rune before the cursor.
|
// We need the rune before the cursor.
|
||||||
iter.BackwardChar()
|
iter.BackwardChar()
|
||||||
|
@ -118,8 +118,8 @@ const searchFlags = 0 |
|
||||||
gtk.TEXT_SEARCH_TEXT_ONLY |
|
gtk.TEXT_SEARCH_TEXT_ONLY |
|
||||||
gtk.TEXT_SEARCH_VISIBLE_ONLY
|
gtk.TEXT_SEARCH_VISIBLE_ONLY
|
||||||
|
|
||||||
func GetWordIters(buf *gtk.TextBuffer, offset int) (start, end *gtk.TextIter) {
|
func GetWordIters(buf *gtk.TextBuffer, offset int64) (start, end *gtk.TextIter) {
|
||||||
iter := buf.GetIterAtOffset(offset)
|
iter := buf.GetIterAtOffset(int(offset))
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,7 @@ func (i *Icon) SetIcon(url string) {
|
||||||
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
|
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Icon) AsyncSetIconer(iconer cchat.Icon, errwrap string) {
|
func (i *Icon) AsyncSetIconer(iconer cchat.Iconer, errwrap string) {
|
||||||
// Reveal to show the placeholder.
|
// Reveal to show the placeholder.
|
||||||
i.SetRevealChild(true)
|
i.SetRevealChild(true)
|
||||||
|
|
||||||
|
|
|
@ -90,8 +90,8 @@ func BindRichLabel(label Labeler) {
|
||||||
bind(label, func(uri string, ptr gdk.Rectangle) bool {
|
bind(label, func(uri string, ptr gdk.Rectangle) bool {
|
||||||
var output = label.Output()
|
var output = label.Output()
|
||||||
|
|
||||||
if mention := output.IsMention(uri); mention != nil {
|
if segment := output.IsMention(uri); segment != nil {
|
||||||
if p := NewPopoverMentioner(label, output.Input, mention); p != nil {
|
if p := NewPopoverMentioner(label, output.Input, segment); p != nil {
|
||||||
p.SetPointingTo(ptr)
|
p.SetPointingTo(ptr)
|
||||||
p.Popup()
|
p.Popup()
|
||||||
}
|
}
|
||||||
|
@ -103,19 +103,24 @@ func BindRichLabel(label Labeler) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func PopoverMentioner(rel gtk.IWidget, input string, mention text.Mentioner) {
|
func PopoverMentioner(rel gtk.IWidget, input string, mention text.Segment) {
|
||||||
if p := NewPopoverMentioner(rel, input, mention); p != nil {
|
if p := NewPopoverMentioner(rel, input, mention); p != nil {
|
||||||
p.Popup()
|
p.Popup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPopoverMentioner(rel gtk.IWidget, input string, mention text.Mentioner) *gtk.Popover {
|
func NewPopoverMentioner(rel gtk.IWidget, input string, segment text.Segment) *gtk.Popover {
|
||||||
var info = mention.MentionInfo()
|
var mention = segment.AsMentioner()
|
||||||
if info.Empty() {
|
if mention == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
start, end := mention.Bounds()
|
var info = mention.MentionInfo()
|
||||||
|
if info.IsEmpty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
start, end := segment.Bounds()
|
||||||
h := input[start:end]
|
h := input[start:end]
|
||||||
|
|
||||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
@ -125,12 +130,11 @@ func NewPopoverMentioner(rel gtk.IWidget, input string, mention text.Mentioner)
|
||||||
var url string
|
var url string
|
||||||
var round bool
|
var round bool
|
||||||
|
|
||||||
switch v := mention.(type) {
|
if avatarer := segment.AsAvatarer(); avatarer != nil {
|
||||||
case text.MentionerImage:
|
url = avatarer.Avatar()
|
||||||
url = v.Image()
|
|
||||||
case text.MentionerAvatar:
|
|
||||||
url = v.Avatar()
|
|
||||||
round = true
|
round = true
|
||||||
|
} else if imager := segment.AsImager(); imager != nil {
|
||||||
|
url = imager.Image()
|
||||||
}
|
}
|
||||||
|
|
||||||
if url != "" {
|
if url != "" {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/alecthomas/chroma/styles"
|
"github.com/alecthomas/chroma/styles"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap"
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -40,15 +39,14 @@ func Tokenize(language, source string) chroma.Iterator {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
func Segments(appendmap *attrmap.AppendMap, src string, seg text.Codeblocker) {
|
func Segments(appendmap *attrmap.AppendMap, src string, start, end int, lang string) {
|
||||||
var start, end = seg.Bounds()
|
|
||||||
appendmap.Span(
|
appendmap.Span(
|
||||||
start, end,
|
start, end,
|
||||||
`font_family="monospace"`,
|
`font_family="monospace"`,
|
||||||
`insert_hyphens="false"`, // all my homies hate hyphens
|
`insert_hyphens="false"`, // all my homies hate hyphens
|
||||||
)
|
)
|
||||||
|
|
||||||
if i := Tokenize(seg.CodeblockLanguage(), src[start:end]); i != nil {
|
if i := Tokenize(lang, src[start:end]); i != nil {
|
||||||
fmtter.segments(appendmap, start, i)
|
fmtter.segments(appendmap, start, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,14 +25,20 @@ func hyphenate(text string) string {
|
||||||
type RenderOutput struct {
|
type RenderOutput struct {
|
||||||
Markup string
|
Markup string
|
||||||
Input string // useless to keep parts, as Go will keep all alive anyway
|
Input string // useless to keep parts, as Go will keep all alive anyway
|
||||||
Mentions []text.Mentioner
|
Mentions []MentionSegment
|
||||||
|
}
|
||||||
|
|
||||||
|
// MentionSegment is a type that satisfies both Segment and Mentioner.
|
||||||
|
type MentionSegment struct {
|
||||||
|
text.Segment
|
||||||
|
text.Mentioner
|
||||||
}
|
}
|
||||||
|
|
||||||
// f_Mention is used to print and parse mention URIs.
|
// f_Mention is used to print and parse mention URIs.
|
||||||
const f_Mention = "cchat://mention/%d" // %d == Mentions[i]
|
const f_Mention = "cchat://mention/%d" // %d == Mentions[i]
|
||||||
|
|
||||||
// IsMention returns the mention if the URI is correct, or nil if none.
|
// IsMention returns the mention if the URI is correct, or nil if none.
|
||||||
func (r RenderOutput) IsMention(uri string) text.Mentioner {
|
func (r RenderOutput) IsMention(uri string) text.Segment {
|
||||||
var i int
|
var i int
|
||||||
|
|
||||||
if _, err := fmt.Sscanf(uri, f_Mention, &i); err != nil {
|
if _, err := fmt.Sscanf(uri, f_Mention, &i); err != nil {
|
||||||
|
@ -98,34 +104,36 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
|
||||||
var appended = attrmap.NewAppendedMap()
|
var appended = attrmap.NewAppendedMap()
|
||||||
|
|
||||||
// map to store mentions
|
// map to store mentions
|
||||||
var mentions []text.Mentioner
|
var mentions []MentionSegment
|
||||||
|
|
||||||
// Parse all segments.
|
// Parse all segments.
|
||||||
for _, segment := range content.Segments {
|
for _, segment := range content.Segments {
|
||||||
start, end := segment.Bounds()
|
start, end := segment.Bounds()
|
||||||
|
|
||||||
if segment, ok := segment.(text.Linker); ok {
|
if linker := segment.AsLinker(); linker != nil {
|
||||||
appended.Anchor(start, end, segment.Link())
|
appended.Anchor(start, end, linker.Link())
|
||||||
}
|
}
|
||||||
|
|
||||||
if segment, ok := segment.(text.Imager); ok {
|
// Only inline images if start == end per specification.
|
||||||
// Ends don't matter with images.
|
if start == end {
|
||||||
appended.Open(start, composeImageMarkup(segment))
|
if imager := segment.AsImager(); imager != nil {
|
||||||
|
appended.Open(start, composeImageMarkup(imager))
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatarer := segment.AsAvatarer(); avatarer != nil {
|
||||||
|
// Ends don't matter with images.
|
||||||
|
appended.Open(start, composeAvatarMarkup(avatarer))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if segment, ok := segment.(text.Avatarer); ok {
|
if colorer := segment.AsColorer(); colorer != nil {
|
||||||
// Ends don't matter with images.
|
appended.Span(start, end, colorAttrs(colorer.Color(), false)...)
|
||||||
appended.Open(start, composeAvatarMarkup(segment))
|
|
||||||
}
|
|
||||||
|
|
||||||
if segment, ok := segment.(text.Colorer); ok {
|
|
||||||
appended.Span(start, end, fmt.Sprintf("color=\"#%06X\"", segment.Color()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mentioner needs to be before colorer, as we'd want the below color
|
// Mentioner needs to be before colorer, as we'd want the below color
|
||||||
// segment to also highlight the full mention as well as make the
|
// segment to also highlight the full mention as well as make the
|
||||||
// padding part of the hyperlink.
|
// padding part of the hyperlink.
|
||||||
if segment, ok := segment.(text.Mentioner); ok {
|
if mentioner := segment.AsMentioner(); mentioner != nil {
|
||||||
// Render the mention into "cchat://mention:0" or such. Other
|
// Render the mention into "cchat://mention:0" or such. Other
|
||||||
// components will take care of showing the information.
|
// components will take care of showing the information.
|
||||||
if !cfg.NoMentionLinks {
|
if !cfg.NoMentionLinks {
|
||||||
|
@ -133,32 +141,35 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the mention segment into the list regardless of hyperlinks.
|
// Add the mention segment into the list regardless of hyperlinks.
|
||||||
mentions = append(mentions, segment)
|
mentions = append(mentions, MentionSegment{
|
||||||
|
Segment: segment,
|
||||||
|
Mentioner: mentioner,
|
||||||
|
})
|
||||||
|
|
||||||
if segment, ok := segment.(text.Colorer); ok {
|
if colorer := segment.AsColorer(); colorer != nil {
|
||||||
// Add a dimmed background highlight and pad the button-like
|
// Only pad the name and add a dimmed background if the bounds
|
||||||
// link.
|
// do not cover the whole segment.
|
||||||
appended.Span(
|
var cover = (start == 0) && (end == len(content.Content))
|
||||||
start, end,
|
appended.Span(start, end, colorAttrs(colorer.Color(), !cover)...)
|
||||||
"bgalpha=\"10%\"",
|
if !cover {
|
||||||
fmt.Sprintf("bgcolor=\"#%06X\"", segment.Color()),
|
appended.Pad(start, end)
|
||||||
)
|
}
|
||||||
appended.Pad(start, end)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if segment, ok := segment.(text.Attributor); ok {
|
if attributor := segment.AsAttributor(); attributor != nil {
|
||||||
appended.Span(start, end, markupAttr(segment.Attribute()))
|
appended.Span(start, end, markupAttr(attributor.Attribute()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if segment, ok := segment.(text.Codeblocker); ok {
|
if codeblocker := segment.AsCodeblocker(); codeblocker != nil {
|
||||||
|
start, end := segment.Bounds()
|
||||||
// Syntax highlight the codeblock.
|
// Syntax highlight the codeblock.
|
||||||
hl.Segments(&appended, content.Content, segment)
|
hl.Segments(&appended, content.Content, start, end, codeblocker.CodeblockLanguage())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make this not shit. Maybe make it somehow not rely on green
|
// TODO: make this not shit. Maybe make it somehow not rely on green
|
||||||
// arrows. Or maybe.
|
// arrows. Or maybe.
|
||||||
if _, ok := segment.(text.Quoteblocker); ok {
|
if segment.AsQuoteblocker() != nil {
|
||||||
appended.Span(start, end, `color="#789922"`)
|
appended.Span(start, end, `color="#789922"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,19 +192,37 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func color(c uint32, bg bool) []string {
|
// splitRGBA splits the given rgba integer into rgb and a.
|
||||||
var hex = fmt.Sprintf("#%06X", c)
|
func splitRGBA(rgba uint32) (rgb, a uint32) {
|
||||||
|
rgb = rgba >> 8 // extract the RGB bits
|
||||||
|
a = rgba & 0xFF // extract the A bits
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var attrs = []string{
|
// colorAttrs renders the given color into a list of attributes.
|
||||||
fmt.Sprintf(`color="%s"`, hex),
|
func colorAttrs(c uint32, bg bool) []string {
|
||||||
|
// Split the RGBA color value to calculate.
|
||||||
|
rgb, a := splitRGBA(c)
|
||||||
|
|
||||||
|
// Render the hex representation beforehand.
|
||||||
|
hex := fmt.Sprintf("#%06X", rgb)
|
||||||
|
|
||||||
|
attrs := make([]string, 1, 4)
|
||||||
|
attrs[0] = fmt.Sprintf(`color="%s"`, hex)
|
||||||
|
|
||||||
|
// If we have an alpha that isn't solid (100%), then write it.
|
||||||
|
if a < 0xFF {
|
||||||
|
// Calculate alpha percentage.
|
||||||
|
perc := a * 100 / 255
|
||||||
|
attrs = append(attrs, fmt.Sprintf(`fgalpha="%d%%"`, perc))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw a faded background if we explicitly requested for one.
|
||||||
if bg {
|
if bg {
|
||||||
attrs = append(
|
// Calculate how faded the background should be for visual purposes.
|
||||||
attrs,
|
perc := a * 10 / 255 // always 10% or less.
|
||||||
`bgalpha="10%"`,
|
attrs = append(attrs, fmt.Sprintf(`bgalpha="%d%%"`, perc))
|
||||||
fmt.Sprintf(`bgcolor="%s"`, hex),
|
attrs = append(attrs, fmt.Sprintf(`bgcolor="%s"`, hex))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
@ -274,25 +303,25 @@ func markupAttr(attr text.Attribute) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
var attrs = make([]string, 0, 1)
|
var attrs = make([]string, 0, 1)
|
||||||
if attr.Has(text.AttrBold) {
|
if attr.Has(text.AttributeBold) {
|
||||||
attrs = append(attrs, `weight="bold"`)
|
attrs = append(attrs, `weight="bold"`)
|
||||||
}
|
}
|
||||||
if attr.Has(text.AttrItalics) {
|
if attr.Has(text.AttributeItalics) {
|
||||||
attrs = append(attrs, `style="italic"`)
|
attrs = append(attrs, `style="italic"`)
|
||||||
}
|
}
|
||||||
if attr.Has(text.AttrUnderline) {
|
if attr.Has(text.AttributeUnderline) {
|
||||||
attrs = append(attrs, `underline="single"`)
|
attrs = append(attrs, `underline="single"`)
|
||||||
}
|
}
|
||||||
if attr.Has(text.AttrStrikethrough) {
|
if attr.Has(text.AttributeStrikethrough) {
|
||||||
attrs = append(attrs, `strikethrough="true"`)
|
attrs = append(attrs, `strikethrough="true"`)
|
||||||
}
|
}
|
||||||
if attr.Has(text.AttrSpoiler) {
|
if attr.Has(text.AttributeSpoiler) {
|
||||||
attrs = append(attrs, `alpha="35%"`) // no fancy click here
|
attrs = append(attrs, `alpha="35%"`) // no fancy click here
|
||||||
}
|
}
|
||||||
if attr.Has(text.AttrMonospace) {
|
if attr.Has(text.AttributeMonospace) {
|
||||||
attrs = append(attrs, `font_family="monospace"`)
|
attrs = append(attrs, `font_family="monospace"`)
|
||||||
}
|
}
|
||||||
if attr.Has(text.AttrDimmed) {
|
if attr.Has(text.AttributeDimmed) {
|
||||||
attrs = append(attrs, `alpha="35%"`)
|
attrs = append(attrs, `alpha="35%"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ViewController interface {
|
type ViewController interface {
|
||||||
RowSelected(*session.Row, *server.ServerRow, cchat.ServerMessage)
|
MessengerSelected(*session.Row, *server.ServerRow)
|
||||||
SessionSelected(*Service, *session.Row)
|
SessionSelected(*Service, *session.Row)
|
||||||
AuthenticateSession(*List, *Service)
|
AuthenticateSession(*List, *Service)
|
||||||
OnSessionRemove(*Service, *session.Row)
|
OnSessionRemove(*Service, *session.Row)
|
||||||
|
|
|
@ -91,18 +91,22 @@ func Save() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Update(b traverse.Breadcrumber, expanded bool) {
|
func Update(b traverse.Breadcrumber, expanded bool) {
|
||||||
var path = traverse.TryID(b)
|
if expanded {
|
||||||
var node = paths
|
var path = traverse.TryID(b)
|
||||||
|
var node = paths
|
||||||
|
|
||||||
// Descend and initialize.
|
// Descend and initialize.
|
||||||
for i := 0; i < len(path); i++ {
|
for i := 0; i < len(path); i++ {
|
||||||
ch, ok := node[path[i]]
|
ch, ok := node[path[i]]
|
||||||
if !ok {
|
if !ok {
|
||||||
ch = make(pathMap)
|
ch = make(pathMap)
|
||||||
node[path[i]] = ch
|
node[path[i]] = ch
|
||||||
|
}
|
||||||
|
|
||||||
|
node = ch
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
node = ch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Save()
|
Save()
|
||||||
|
|
|
@ -20,8 +20,8 @@ import (
|
||||||
const IconSize = 48
|
const IconSize = 48
|
||||||
|
|
||||||
type ListController interface {
|
type ListController interface {
|
||||||
// RowSelected is called when a server message row is clicked.
|
// MessengerSelected is called when a server message row is clicked.
|
||||||
RowSelected(*session.Row, *server.ServerRow, cchat.ServerMessage)
|
MessengerSelected(*session.Row, *server.ServerRow)
|
||||||
// SessionSelected tells the view to change the session view.
|
// SessionSelected tells the view to change the session view.
|
||||||
SessionSelected(*Service, *session.Row)
|
SessionSelected(*Service, *session.Row)
|
||||||
// AuthenticateSession tells View to call to the parent's authenticator.
|
// AuthenticateSession tells View to call to the parent's authenticator.
|
||||||
|
@ -109,7 +109,7 @@ func NewService(svc cchat.Service, svclctrl ListController) *Service {
|
||||||
service.Icon.SetTooltipMarkup(markup.Render(svc.Name()))
|
service.Icon.SetTooltipMarkup(markup.Render(svc.Name()))
|
||||||
serviceIconCSS(service.Icon)
|
serviceIconCSS(service.Icon)
|
||||||
|
|
||||||
if iconer, ok := svc.(cchat.Icon); ok {
|
if iconer := svc.AsIconer(); iconer != nil {
|
||||||
service.Icon.AsyncSetIconer(iconer, "Failed to set service icon")
|
service.Icon.AsyncSetIconer(iconer, "Failed to set service icon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +156,10 @@ func (s *Service) AuthenticateSession() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AddLoadingSession(id, name string) *session.Row {
|
func (s *Service) AddLoadingSession(id, name string) *session.Row {
|
||||||
|
if srow := s.BodyList.Session(id); srow != nil {
|
||||||
|
return srow
|
||||||
|
}
|
||||||
|
|
||||||
srow := session.NewLoading(s, id, name, s)
|
srow := session.NewLoading(s, id, name, s)
|
||||||
srow.Show()
|
srow.Show()
|
||||||
|
|
||||||
|
@ -163,7 +167,13 @@ func (s *Service) AddLoadingSession(id, name string) *session.Row {
|
||||||
return srow
|
return srow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddSession adds the given session. It returns nil if the session already
|
||||||
|
// exists with the given ID.
|
||||||
func (s *Service) AddSession(ses cchat.Session) *session.Row {
|
func (s *Service) AddSession(ses cchat.Session) *session.Row {
|
||||||
|
if srow := s.BodyList.Session(ses.ID()); srow != nil {
|
||||||
|
return srow
|
||||||
|
}
|
||||||
|
|
||||||
srow := session.New(s, ses, s)
|
srow := session.New(s, ses, s)
|
||||||
srow.Show()
|
srow.Show()
|
||||||
|
|
||||||
|
@ -187,19 +197,15 @@ func (s *Service) OnSessionDisconnect(row *session.Row) {
|
||||||
}
|
}
|
||||||
|
|
||||||
s.svclctrl.OnSessionDisconnect(s, row)
|
s.svclctrl.OnSessionDisconnect(s, row)
|
||||||
|
|
||||||
// WHY WAS THIS HERE?!?!?!
|
|
||||||
// s.BodyList.RemoveSessionRow(row.Session.ID())
|
|
||||||
// s.SaveAllSessions()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RowSelected(r *session.Row, sv *server.ServerRow, m cchat.ServerMessage) {
|
func (s *Service) MessengerSelected(r *session.Row, sv *server.ServerRow) {
|
||||||
s.svclctrl.RowSelected(r, sv, m)
|
s.svclctrl.MessengerSelected(r, sv)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RemoveSession(row *session.Row) {
|
func (s *Service) RemoveSession(row *session.Row) {
|
||||||
s.svclctrl.OnSessionRemove(s, row)
|
s.svclctrl.OnSessionRemove(s, row)
|
||||||
s.BodyList.RemoveSessionRow(row.Session.ID())
|
s.BodyList.RemoveSessionRow(row.ID())
|
||||||
s.SaveAllSessions()
|
s.SaveAllSessions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,13 +236,13 @@ func (s *Service) SaveAllSessions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RestoreSession(row *session.Row, id string) {
|
func (s *Service) RestoreSession(row *session.Row, id string) {
|
||||||
rs, ok := s.service.(cchat.SessionRestorer)
|
rs := s.service.AsSessionRestorer()
|
||||||
if !ok {
|
if rs == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if k := keyring.RestoreSession(s.service, id); k != nil {
|
if k := keyring.RestoreSession(s.service, id); k != nil {
|
||||||
restoreAsync(row, rs, *k)
|
row.RestoreSession(rs, *k)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,19 +254,14 @@ func (s *Service) RestoreSession(row *session.Row, id string) {
|
||||||
|
|
||||||
// restoreAll restores all sessions.
|
// restoreAll restores all sessions.
|
||||||
func (s *Service) restoreAll() {
|
func (s *Service) restoreAll() {
|
||||||
rs, ok := s.service.(cchat.SessionRestorer)
|
rs := s.service.AsSessionRestorer()
|
||||||
if !ok {
|
if rs == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session is not a pointer, so we can pass it into arguments safely.
|
// Session is not a pointer, so we can pass it into arguments safely.
|
||||||
for _, ses := range keyring.RestoreSessions(s.service) {
|
for _, ses := range keyring.RestoreSessions(s.service) {
|
||||||
row := s.AddLoadingSession(ses.ID, ses.Name)
|
row := s.AddLoadingSession(ses.ID, ses.Name)
|
||||||
restoreAsync(row, rs, ses)
|
row.RestoreSession(rs, ses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// restoreAsync asynchronously restores a single session.
|
|
||||||
func restoreAsync(r *session.Row, res cchat.SessionRestorer, k keyring.Session) {
|
|
||||||
r.RestoreSession(res, k)
|
|
||||||
}
|
|
||||||
|
|
|
@ -84,7 +84,20 @@ func (sl *List) Sessions() []*Row {
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session returns the session row with the given ID. A nil Row is returned if
|
||||||
|
// none is found.
|
||||||
|
func (sl *List) Session(id string) *Row {
|
||||||
|
row, _ := sl.sessions[id]
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSessionRow adds the given row as a session into the list.
|
||||||
func (sl *List) AddSessionRow(id string, row *Row) {
|
func (sl *List) AddSessionRow(id string, row *Row) {
|
||||||
|
// !!! IMPORTANT !!! Guarantee that there is NO collision.
|
||||||
|
if _, ok := sl.sessions[id]; ok {
|
||||||
|
panic("BUG: Duplicate session; AddSessionRow caller did not check Session.")
|
||||||
|
}
|
||||||
|
|
||||||
// Insert the row RIGHT BEFORE the add button.
|
// Insert the row RIGHT BEFORE the add button.
|
||||||
sl.ListBox.Insert(row, len(sl.sessions))
|
sl.ListBox.Insert(row, len(sl.sessions))
|
||||||
// Set the map, which increases the length by 1.
|
// Set the map, which increases the length by 1.
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
RowSelected(*ServerRow, cchat.ServerMessage)
|
MessengerSelected(*ServerRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Children is a children server with a reference to the parent. By default, a
|
// Children is a children server with a reference to the parent. By default, a
|
||||||
|
@ -170,6 +170,56 @@ func (c *Children) SetServers(servers []cchat.Server) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Children) findID(id cchat.ID) (int, *ServerRow) {
|
||||||
|
for i, row := range c.Rows {
|
||||||
|
if row.Server.ID() == id {
|
||||||
|
return i, row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Children) insertAt(row *ServerRow, i int) {
|
||||||
|
c.Rows = append(c.Rows[:i], append([]*ServerRow{row}, c.Rows[i:]...)...)
|
||||||
|
|
||||||
|
if !c.IsHollow() {
|
||||||
|
c.Box.Add(row)
|
||||||
|
c.Box.ReorderChild(row, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Children) UpdateServer(update cchat.ServerUpdate) {
|
||||||
|
gts.ExecAsync(func() {
|
||||||
|
prevID, replace := update.PreviousID()
|
||||||
|
|
||||||
|
// TODO: I don't think this code unhollows a new server.
|
||||||
|
var newServer = NewHollowServer(c, update, c.rowctrl)
|
||||||
|
var i, oldRow = c.findID(prevID)
|
||||||
|
|
||||||
|
// If we're appending a new row, then replace is false.
|
||||||
|
if !replace {
|
||||||
|
// Increment the old row's index so we know where to insert.
|
||||||
|
c.insertAt(newServer, i+1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update the server if the old row was found.
|
||||||
|
if oldRow == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Rows[i] = newServer
|
||||||
|
|
||||||
|
if !c.IsHollow() {
|
||||||
|
// Update the UI as well.
|
||||||
|
// TODO: check if this reorder is correct.
|
||||||
|
c.Box.Remove(oldRow)
|
||||||
|
c.Box.Add(newServer)
|
||||||
|
c.Box.ReorderChild(newServer, i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// LoadAll forces all children rows to be unhollowed (initialized). It does
|
// LoadAll forces all children rows to be unhollowed (initialized). It does
|
||||||
// NOT check if the children container itself is hollow.
|
// NOT check if the children container itself is hollow.
|
||||||
func (c *Children) LoadAll() {
|
func (c *Children) LoadAll() {
|
||||||
|
|
|
@ -1,27 +1,23 @@
|
||||||
package commander
|
package commander
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionCommander interface {
|
// Buffer represents an unbuffered API around the text buffer.
|
||||||
cchat.Session
|
|
||||||
cchat.Commander
|
|
||||||
}
|
|
||||||
|
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
*gtk.TextBuffer
|
*gtk.TextBuffer
|
||||||
svcname string
|
name string
|
||||||
cmder SessionCommander
|
cmder cchat.Commander
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBuffer creates a new buffer with the given SessionCommander, or returns
|
// NewBuffer creates a new buffer with the given SessionCommander, or returns
|
||||||
// nil if cmder is nil.
|
// nil if cmder is nil.
|
||||||
func NewBuffer(svc cchat.Service, cmder SessionCommander) *Buffer {
|
func NewBuffer(name string, cmder cchat.Commander) *Buffer {
|
||||||
if cmder == nil {
|
if cmder == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -33,7 +29,7 @@ func NewBuffer(svc cchat.Service, cmder SessionCommander) *Buffer {
|
||||||
b.CreateTag("system", map[string]interface{}{
|
b.CreateTag("system", map[string]interface{}{
|
||||||
"foreground": "#808080",
|
"foreground": "#808080",
|
||||||
})
|
})
|
||||||
return &Buffer{b, svc.Name().Content, cmder}
|
return &Buffer{b, name, cmder}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteError is not thread-safe.
|
// WriteError is not thread-safe.
|
||||||
|
@ -46,15 +42,19 @@ func (b *Buffer) WriteSystem(bytes []byte) {
|
||||||
b.InsertWithTagByName(b.GetEndIter(), string(bytes), "system")
|
b.InsertWithTagByName(b.GetEndIter(), string(bytes), "system")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printlnf is not thread-safe.
|
// Systemlnf is not thread-safe.
|
||||||
func (b *Buffer) Printlnf(f string, v ...interface{}) {
|
func (b *Buffer) Systemlnf(f string, v ...interface{}) {
|
||||||
b.WriteSystem([]byte(fmt.Sprintf(f+"\n", v...)))
|
b.WriteSystem([]byte(fmt.Sprintf(f+"\n", v...)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write is thread-safe.
|
func (b *Buffer) WriteOutput(output []byte) {
|
||||||
func (b *Buffer) Write(bytes []byte) (int, error) {
|
var iter = b.GetEndIter()
|
||||||
gts.ExecAsync(func() { b.Insert(b.GetEndIter(), string(bytes)) })
|
|
||||||
return len(bytes), nil
|
b.Insert(iter, string(output))
|
||||||
|
|
||||||
|
if !bytes.HasSuffix(output, []byte("\n")) {
|
||||||
|
b.Insert(iter, "\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffer) ShowDialog() {
|
func (b *Buffer) ShowDialog() {
|
|
@ -1,8 +1,6 @@
|
||||||
package commander
|
package commander
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
@ -11,10 +9,9 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||||
|
"github.com/diamondburned/cchat/utils/split"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/gotk3/gotk3/pango"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var monospace = primitives.PrepareCSS(`
|
var monospace = primitives.PrepareCSS(`
|
||||||
|
@ -24,16 +21,12 @@ var monospace = primitives.PrepareCSS(`
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
var commandPadding = primitives.PrepareCSS(`
|
|
||||||
* { padding: 8px 12px; }
|
|
||||||
`)
|
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
|
|
||||||
cmder cchat.Commander
|
cmder cchat.Commander
|
||||||
|
cmplt *completion.Completer
|
||||||
buffer *Buffer
|
buffer *Buffer
|
||||||
cmplt *completer
|
|
||||||
|
|
||||||
inputbuf *gtk.TextBuffer
|
inputbuf *gtk.TextBuffer
|
||||||
|
|
||||||
|
@ -42,14 +35,11 @@ type Session struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func SpawnDialog(buf *Buffer) {
|
func SpawnDialog(buf *Buffer) {
|
||||||
s := NewSession(buf.cmder, buf)
|
s := NewSession(buf)
|
||||||
s.Show()
|
s.Show()
|
||||||
|
|
||||||
h, _ := gtk.HeaderBarNew()
|
h, _ := gtk.HeaderBarNew()
|
||||||
h.SetTitle(fmt.Sprintf(
|
h.SetTitle("Commander: " + buf.name)
|
||||||
"Commander: %s on %s",
|
|
||||||
buf.cmder.Name().Content, buf.svcname,
|
|
||||||
))
|
|
||||||
h.SetShowCloseButton(true)
|
h.SetShowCloseButton(true)
|
||||||
h.Show()
|
h.Show()
|
||||||
|
|
||||||
|
@ -60,12 +50,13 @@ func SpawnDialog(buf *Buffer) {
|
||||||
d.Show()
|
d.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
|
func NewSession(buf *Buffer) *Session {
|
||||||
view, _ := gtk.TextViewNewWithBuffer(buf.TextBuffer)
|
view, _ := gtk.TextViewNewWithBuffer(buf.TextBuffer)
|
||||||
view.SetEditable(false)
|
view.SetEditable(false)
|
||||||
view.SetProperty("monospace", true)
|
view.SetProperty("monospace", true)
|
||||||
view.SetPixelsAboveLines(1)
|
view.SetPixelsAboveLines(1)
|
||||||
view.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
view.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||||
|
view.SetBorderWidth(8)
|
||||||
view.Show()
|
view.Show()
|
||||||
|
|
||||||
scroll := autoscroll.NewScrolledWindow()
|
scroll := autoscroll.NewScrolledWindow()
|
||||||
|
@ -75,6 +66,7 @@ func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
|
||||||
|
|
||||||
input, _ := gtk.TextViewNew()
|
input, _ := gtk.TextViewNew()
|
||||||
input.SetSizeRequest(-1, 35) // magic height 35px
|
input.SetSizeRequest(-1, 35) // magic height 35px
|
||||||
|
input.SetBorderWidth(8)
|
||||||
primitives.AttachCSS(input, monospace)
|
primitives.AttachCSS(input, monospace)
|
||||||
input.Show()
|
input.Show()
|
||||||
|
|
||||||
|
@ -91,11 +83,15 @@ func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
|
||||||
b.PackStart(sep, false, false, 0)
|
b.PackStart(sep, false, false, 0)
|
||||||
b.PackStart(inputscroll, false, false, 0)
|
b.PackStart(inputscroll, false, false, 0)
|
||||||
|
|
||||||
|
completer := completion.NewCompleter(input)
|
||||||
|
completer.Splitter = split.ArgsIndexed
|
||||||
|
completer.SetCompleter(buf.cmder.AsCompleter())
|
||||||
|
|
||||||
session := &Session{
|
session := &Session{
|
||||||
Box: b,
|
Box: b,
|
||||||
cmder: cmder,
|
cmder: buf.cmder,
|
||||||
|
cmplt: completer,
|
||||||
buffer: buf,
|
buffer: buf,
|
||||||
cmplt: newCompleter(input, cmder),
|
|
||||||
inputbuf: inputbuf,
|
inputbuf: inputbuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,8 +101,6 @@ func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
|
||||||
primitives.AddClass(b, "commander")
|
primitives.AddClass(b, "commander")
|
||||||
primitives.AddClass(view, "command-buffer")
|
primitives.AddClass(view, "command-buffer")
|
||||||
primitives.AddClass(input, "command-input")
|
primitives.AddClass(input, "command-input")
|
||||||
primitives.AttachCSS(view, commandPadding)
|
|
||||||
primitives.AttachCSS(input, commandPadding)
|
|
||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
@ -117,14 +111,10 @@ func (s *Session) inputActivate(v *gtk.TextView, ev *gdk.Event) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the slice of words.
|
||||||
|
var words = s.cmplt.Content()
|
||||||
// If the input is empty, then ignore.
|
// If the input is empty, then ignore.
|
||||||
if len(s.cmplt.Words) == 0 {
|
if len(words) == 0 {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := s.cmder.RunCommand(s.cmplt.Words)
|
|
||||||
if err != nil {
|
|
||||||
s.buffer.WriteError(err)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,19 +122,22 @@ func (s *Session) inputActivate(v *gtk.TextView, ev *gdk.Event) bool {
|
||||||
s.inputbuf.Delete(s.inputbuf.GetBounds())
|
s.inputbuf.Delete(s.inputbuf.GetBounds())
|
||||||
|
|
||||||
var then = time.Now()
|
var then = time.Now()
|
||||||
s.buffer.Printlnf("%s: Running command...", then.Format(time.Kitchen))
|
s.buffer.Systemlnf("%s > %q", then.Format(time.Kitchen), words)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_, err := io.Copy(s.buffer, r)
|
out, err := s.cmder.Run(words)
|
||||||
r.Close()
|
|
||||||
|
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
|
if out != nil {
|
||||||
|
s.buffer.WriteOutput(out)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.buffer.WriteError(errors.Wrap(err, "Internal error"))
|
s.buffer.WriteError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = time.Now()
|
var now = time.Now()
|
||||||
s.buffer.Printlnf(
|
s.buffer.Systemlnf(
|
||||||
"%s: Finished running command, took %s.",
|
"%s: Finished running command, took %s.",
|
||||||
now.Format(time.Kitchen),
|
now.Format(time.Kitchen),
|
||||||
now.Sub(then).String(),
|
now.Sub(then).String(),
|
||||||
|
@ -154,47 +147,3 @@ func (s *Session) inputActivate(v *gtk.TextView, ev *gdk.Event) bool {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
type completer struct {
|
|
||||||
*completion.Completer
|
|
||||||
|
|
||||||
completer cchat.CommandCompleter
|
|
||||||
choices []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCompleter(input *gtk.TextView, v cchat.Commander) *completer {
|
|
||||||
completer := &completer{}
|
|
||||||
completer.Completer = completion.NewCompleter(input, completer)
|
|
||||||
|
|
||||||
c, ok := v.(cchat.CommandCompleter)
|
|
||||||
if ok {
|
|
||||||
completer.completer = c
|
|
||||||
}
|
|
||||||
|
|
||||||
return completer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *completer) Update(words []string, offset int) []gtk.IWidget {
|
|
||||||
if c.completer == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.choices = c.completer.CompleteCommand(words, offset)
|
|
||||||
var widgets = make([]gtk.IWidget, 0, len(c.choices))
|
|
||||||
|
|
||||||
for _, choice := range c.choices {
|
|
||||||
l, _ := gtk.LabelNew(choice)
|
|
||||||
l.SetXAlign(0)
|
|
||||||
l.SetEllipsize(pango.ELLIPSIZE_END)
|
|
||||||
primitives.AttachCSS(l, monospace)
|
|
||||||
l.Show()
|
|
||||||
|
|
||||||
widgets = append(widgets, l)
|
|
||||||
}
|
|
||||||
|
|
||||||
return widgets
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *completer) Word(i int) string {
|
|
||||||
return c.choices[i]
|
|
||||||
}
|
|
|
@ -3,14 +3,18 @@ package server
|
||||||
import (
|
import (
|
||||||
"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/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/actions"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/savepath"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/savepath"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/button"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/button"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/commander"
|
||||||
"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/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -26,20 +30,23 @@ func AssertUnhollow(hollower interface{ IsHollow() bool }) {
|
||||||
|
|
||||||
type ServerRow struct {
|
type ServerRow struct {
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
Avatar *roundimage.Avatar
|
Avatar *roundimage.Avatar
|
||||||
Button *button.ToggleButtonImage
|
Button *button.ToggleButtonImage
|
||||||
|
ActionsMenu *actions.Menu
|
||||||
|
|
||||||
|
Server cchat.Server
|
||||||
|
ctrl Controller
|
||||||
|
|
||||||
parentcrumb traverse.Breadcrumber
|
parentcrumb traverse.Breadcrumber
|
||||||
|
|
||||||
|
cmder *commander.Buffer
|
||||||
|
|
||||||
// non-nil if server list and the function returns error
|
// non-nil if server list and the function returns error
|
||||||
childrenErr error
|
childrenErr error
|
||||||
|
|
||||||
childrev *gtk.Revealer
|
childrev *gtk.Revealer
|
||||||
children *Children
|
children *Children
|
||||||
serverList cchat.ServerList
|
serverList cchat.Lister
|
||||||
|
|
||||||
ctrl Controller
|
|
||||||
Server cchat.Server
|
|
||||||
|
|
||||||
// State that's updated even when stale. Initializations will use these.
|
// State that's updated even when stale. Initializations will use these.
|
||||||
unread bool
|
unread bool
|
||||||
|
@ -68,13 +75,18 @@ func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl Controller)
|
||||||
cancelUnread: func() {},
|
cancelUnread: func() {},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch sv := sv.(type) {
|
var (
|
||||||
case cchat.ServerList:
|
lister = sv.AsLister()
|
||||||
serverRow.SetHollowServerList(sv, ctrl)
|
messenger = sv.AsMessenger()
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case lister != nil:
|
||||||
|
serverRow.SetHollowServerList(lister, ctrl)
|
||||||
serverRow.children.SetUnreadHandler(serverRow.SetUnreadUnsafe)
|
serverRow.children.SetUnreadHandler(serverRow.SetUnreadUnsafe)
|
||||||
|
|
||||||
case cchat.ServerMessage:
|
case messenger != nil:
|
||||||
if unreader, ok := sv.(cchat.ServerMessageUnreadIndicator); ok {
|
if unreader := messenger.AsUnreadIndicator(); unreader != nil {
|
||||||
gts.Async(func() (func(), error) {
|
gts.Async(func() (func(), error) {
|
||||||
c, err := unreader.UnreadIndicate(serverRow)
|
c, err := unreader.UnreadIndicate(serverRow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -125,8 +137,29 @@ func (r *ServerRow) Init() {
|
||||||
// Restore the read state.
|
// Restore the read state.
|
||||||
r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state
|
r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state
|
||||||
|
|
||||||
switch server := r.Server.(type) {
|
// Make the Actions menu.
|
||||||
case cchat.ServerList:
|
r.ActionsMenu = actions.NewMenu("server")
|
||||||
|
r.ActionsMenu.InsertActionGroup(r)
|
||||||
|
|
||||||
|
if cmder := r.Server.AsCommander(); cmder != nil {
|
||||||
|
r.cmder = commander.NewBuffer(r.Server.Name().String(), cmder)
|
||||||
|
r.ActionsMenu.AddAction("Command Prompt", r.cmder.ShowDialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind right clicks and show a popover menu on such event.
|
||||||
|
r.Button.Connect("button-press-event", func(_ gtk.IWidget, ev *gdk.Event) {
|
||||||
|
if gts.EventIsRightClick(ev) {
|
||||||
|
r.ActionsMenu.Popover(r).Popup()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var (
|
||||||
|
lister = r.Server.AsLister()
|
||||||
|
messenger = r.Server.AsMessenger()
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case lister != nil:
|
||||||
primitives.AddClass(r, "server-list")
|
primitives.AddClass(r, "server-list")
|
||||||
r.children.Init()
|
r.children.Init()
|
||||||
r.children.Show()
|
r.children.Show()
|
||||||
|
@ -139,9 +172,9 @@ func (r *ServerRow) Init() {
|
||||||
r.Box.PackStart(r.childrev, false, false, 0)
|
r.Box.PackStart(r.childrev, false, false, 0)
|
||||||
r.Button.SetClicked(r.SetRevealChild)
|
r.Button.SetClicked(r.SetRevealChild)
|
||||||
|
|
||||||
case cchat.ServerMessage:
|
case messenger != nil:
|
||||||
primitives.AddClass(r, "server-message")
|
primitives.AddClass(r, "server-message")
|
||||||
r.Button.SetClicked(func(bool) { r.ctrl.RowSelected(r, server) })
|
r.Button.SetClicked(func(bool) { r.ctrl.MessengerSelected(r) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +221,7 @@ func (r *ServerRow) IsHollow() bool {
|
||||||
|
|
||||||
// SetHollowServerList sets the row to a hollow server list (children) and
|
// SetHollowServerList sets the row to a hollow server list (children) and
|
||||||
// recursively create
|
// recursively create
|
||||||
func (r *ServerRow) SetHollowServerList(list cchat.ServerList, ctrl Controller) {
|
func (r *ServerRow) SetHollowServerList(list cchat.Lister, ctrl Controller) {
|
||||||
r.serverList = list
|
r.serverList = list
|
||||||
|
|
||||||
r.children = NewHollowChildren(r, ctrl)
|
r.children = NewHollowChildren(r, ctrl)
|
||||||
|
@ -196,6 +229,9 @@ func (r *ServerRow) SetHollowServerList(list cchat.ServerList, ctrl Controller)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
var err = list.Servers(r.children)
|
var err = list.Servers(r.children)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "Failed to get servers"))
|
||||||
|
}
|
||||||
|
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
// Announce that we're not loading anymore.
|
// Announce that we're not loading anymore.
|
||||||
|
@ -224,6 +260,7 @@ func (r *ServerRow) Reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the state.
|
// Reset the state.
|
||||||
|
r.ActionsMenu.Reset()
|
||||||
r.serverList = nil
|
r.serverList = nil
|
||||||
r.children = nil
|
r.children = nil
|
||||||
}
|
}
|
||||||
|
@ -279,10 +316,11 @@ func (r *ServerRow) SetLabelUnsafe(name text.Rich) {
|
||||||
r.Avatar.SetText(name.Content)
|
r.Avatar.SetText(name.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ServerRow) SetIconer(v interface{}) {
|
// SetIconer takes in a Namer for AsIconer.
|
||||||
|
func (r *ServerRow) SetIconer(v cchat.Namer) {
|
||||||
AssertUnhollow(r)
|
AssertUnhollow(r)
|
||||||
|
|
||||||
if iconer, ok := v.(cchat.Icon); ok {
|
if iconer := v.AsIconer(); iconer != nil {
|
||||||
r.Button.Image.SetSize(IconSize)
|
r.Button.Image.SetSize(IconSize)
|
||||||
r.Button.Image.AsyncSetIconer(iconer, "Error getting server icon URL")
|
r.Button.Image.AsyncSetIconer(iconer, "Error getting server icon URL")
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ type Servers struct {
|
||||||
spinner *spinner.Boxed // non-nil if loading.
|
spinner *spinner.Boxed // non-nil if loading.
|
||||||
|
|
||||||
// state
|
// state
|
||||||
ServerList cchat.ServerList
|
ServerList cchat.Lister
|
||||||
}
|
}
|
||||||
|
|
||||||
var toplevelCSS = primitives.PrepareClassCSS("top-level", `
|
var toplevelCSS = primitives.PrepareClassCSS("top-level", `
|
||||||
|
@ -72,7 +72,7 @@ func (s *Servers) IsLoading() bool {
|
||||||
|
|
||||||
// SetList indicates that the server list has been loaded. Unlike
|
// SetList indicates that the server list has been loaded. Unlike
|
||||||
// server.Children, this method will load immediately.
|
// server.Children, this method will load immediately.
|
||||||
func (s *Servers) SetList(slist cchat.ServerList) {
|
func (s *Servers) SetList(slist cchat.Lister) {
|
||||||
primitives.RemoveChildren(s)
|
primitives.RemoveChildren(s)
|
||||||
s.ServerList = slist
|
s.ServerList = slist
|
||||||
s.load()
|
s.load()
|
||||||
|
|
|
@ -12,9 +12,9 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/spinner"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/spinner"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/commander"
|
|
||||||
"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/button"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/button"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/commander"
|
||||||
"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/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
@ -35,9 +35,9 @@ type Servicer interface {
|
||||||
// SessionSelected is called when the row is clicked. The parent container
|
// SessionSelected is called when the row is clicked. The parent container
|
||||||
// should change the views to show this session's *Servers.
|
// should change the views to show this session's *Servers.
|
||||||
SessionSelected(*Row)
|
SessionSelected(*Row)
|
||||||
// RowSelected is called when a server that can display messages (aka
|
// MessengerSelected is called when a server that can display messages (aka
|
||||||
// implements ServerMessage) is called.
|
// implements Messenger) is called.
|
||||||
RowSelected(*Row, *server.ServerRow, cchat.ServerMessage)
|
MessengerSelected(*Row, *server.ServerRow)
|
||||||
// RestoreSession is called with the session ID to ask the controller to
|
// RestoreSession is called with the session ID to ask the controller to
|
||||||
// restore it from keyring information.
|
// restore it from keyring information.
|
||||||
RestoreSession(*Row, string) // ID string, async
|
RestoreSession(*Row, string) // ID string, async
|
||||||
|
@ -312,7 +312,7 @@ func (r *Row) SetSession(ses cchat.Session) {
|
||||||
r.avatar.SetText(ses.Name().Content)
|
r.avatar.SetText(ses.Name().Content)
|
||||||
|
|
||||||
// If the session has an icon, then use it.
|
// If the session has an icon, then use it.
|
||||||
if iconer, ok := ses.(cchat.Icon); ok {
|
if iconer := ses.AsIconer(); iconer != nil {
|
||||||
r.icon.Icon.AsyncSetIconer(iconer, "Failed to set session icon")
|
r.icon.Icon.AsyncSetIconer(iconer, "Failed to set session icon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,11 +330,10 @@ func (r *Row) SetSession(ses cchat.Session) {
|
||||||
// Set the commander, if any. The function will return nil if the assertion
|
// Set the commander, if any. The function will return nil if the assertion
|
||||||
// returns nil. As such, we assert with an ignored ok bool, allowing cmd to
|
// returns nil. As such, we assert with an ignored ok bool, allowing cmd to
|
||||||
// be nil.
|
// be nil.
|
||||||
cmd, _ := ses.(commander.SessionCommander)
|
if cmder := ses.AsCommander(); cmder != nil {
|
||||||
r.cmder = commander.NewBuffer(r.svcctrl.Service(), cmd)
|
r.cmder = commander.NewBuffer(ses.Name().String(), cmder)
|
||||||
|
// Show the command button if the session actually supports the
|
||||||
// Show the command button if the session actually supports the commander.
|
// commander.
|
||||||
if r.cmder != nil {
|
|
||||||
r.ActionsMenu.AddAction("Command Prompt", r.ShowCommander)
|
r.ActionsMenu.AddAction("Command Prompt", r.ShowCommander)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,8 +341,8 @@ func (r *Row) SetSession(ses cchat.Session) {
|
||||||
r.Servers.SetList(ses)
|
r.Servers.SetList(ses)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Row) RowSelected(sr *server.ServerRow, smsg cchat.ServerMessage) {
|
func (r *Row) MessengerSelected(sr *server.ServerRow) {
|
||||||
r.svcctrl.RowSelected(r, sr, smsg)
|
r.svcctrl.MessengerSelected(r, sr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveSession removes itself from the session list.
|
// RemoveSession removes itself from the session list.
|
||||||
|
@ -351,10 +350,15 @@ func (r *Row) RemoveSession() {
|
||||||
// Remove the session off the list.
|
// Remove the session off the list.
|
||||||
r.svcctrl.RemoveSession(r)
|
r.svcctrl.RemoveSession(r)
|
||||||
|
|
||||||
|
var session = r.Session
|
||||||
|
if session == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Asynchrously disconnect.
|
// Asynchrously disconnect.
|
||||||
go func() {
|
go func() {
|
||||||
if err := r.Session.Disconnect(); err != nil {
|
if err := session.Disconnect(); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Non-fatal, failed to disconnect removed session"))
|
log.Error(errors.Wrap(err, "non-fatal; failed to disconnect removed session"))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ import (
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
// SessionSelected is called when
|
// SessionSelected is called when
|
||||||
SessionSelected(svc *Service, srow *session.Row)
|
SessionSelected(svc *Service, srow *session.Row)
|
||||||
// RowSelected is wrapped around session's MessageRowSelected.
|
// MessengerSelected is wrapped around session's MessengerSelected.
|
||||||
RowSelected(*session.Row, *server.ServerRow, cchat.ServerMessage)
|
MessengerSelected(*session.Row, *server.ServerRow)
|
||||||
// AuthenticateSession is called to spawn the authentication dialog.
|
// AuthenticateSession is called to spawn the authentication dialog.
|
||||||
AuthenticateSession(*List, *Service)
|
AuthenticateSession(*List, *Service)
|
||||||
// OnSessionRemove is called to remove a session. This should also clear out
|
// OnSessionRemove is called to remove a session. This should also clear out
|
||||||
|
@ -102,11 +102,11 @@ func (v *View) SessionSelected(svc *Service, srow *session.Row) {
|
||||||
v.Controller.SessionSelected(svc, srow)
|
v.Controller.SessionSelected(svc, srow)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RowSelected is called when a row is selected. It updates the header then
|
// MessengerSelected is called when a row is selected. It updates the header
|
||||||
// calls the application's RowSelected method.
|
// then calls the application's RowSelected method.
|
||||||
func (v *View) RowSelected(srow *session.Row, srv *server.ServerRow, smsg cchat.ServerMessage) {
|
func (v *View) MessengerSelected(srow *session.Row, srv *server.ServerRow) {
|
||||||
v.Header.SetBreadcrumber(srv)
|
v.Header.SetBreadcrumber(srv)
|
||||||
v.Controller.RowSelected(srow, srv, smsg)
|
v.Controller.MessengerSelected(srow, srv)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) OnSessionRemove(s *Service, r *session.Row) {
|
func (v *View) OnSessionRemove(s *Service, r *session.Row) {
|
||||||
|
|
|
@ -132,7 +132,7 @@ func (app *App) SessionSelected(svc *service.Service, ses *session.Row) {
|
||||||
app.MessageView.Reset()
|
app.MessageView.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) RowSelected(ses *session.Row, srv *server.ServerRow, smsg cchat.ServerMessage) {
|
func (app *App) MessengerSelected(ses *session.Row, srv *server.ServerRow) {
|
||||||
// Change to the message view.
|
// Change to the message view.
|
||||||
app.Leaflet.SetVisibleChild(app.MessageView)
|
app.Leaflet.SetVisibleChild(app.MessageView)
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ func (app *App) RowSelected(ses *session.Row, srv *server.ServerRow, smsg cchat.
|
||||||
app.lastSelector = srv.SetSelected
|
app.lastSelector = srv.SetSelected
|
||||||
app.lastSelector(true)
|
app.lastSelector(true)
|
||||||
|
|
||||||
app.MessageView.JoinServer(ses.Session, smsg.(messages.ServerMessage), srv)
|
app.MessageView.JoinServer(ses.Session, srv.Server, srv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageView methods.
|
// MessageView methods.
|
||||||
|
|
Loading…
Reference in New Issue