Added partial member list support
This commit is contained in:
parent
fdede5fc13
commit
af2fe85666
7
go.mod
7
go.mod
|
@ -2,13 +2,13 @@ module github.com/diamondburned/cchat-gtk
|
|||
|
||||
go 1.14
|
||||
|
||||
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d
|
||||
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200816224505-3cd69b83a48a
|
||||
|
||||
require (
|
||||
github.com/Xuanwo/go-locale v0.2.0
|
||||
github.com/alecthomas/chroma v0.7.3
|
||||
github.com/diamondburned/cchat v0.0.46
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200730000036-2c93cdc1974e
|
||||
github.com/diamondburned/cchat v0.0.48
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200816234747-9647ad0709f0
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b
|
||||
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
|
@ -21,5 +21,6 @@ require (
|
|||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/twmb/murmur3 v1.1.3
|
||||
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
gopkg.in/yaml.v2 v2.2.7 // indirect
|
||||
)
|
||||
|
|
21
go.sum
21
go.sum
|
@ -46,24 +46,44 @@ github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb h1:Ja/niwykeFoSk
|
|||
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb/go.mod h1:q1MbMBfZrv7xqV8n7LgMwhHs3oBbNwWJes8exs2AmDs=
|
||||
github.com/diamondburned/arikawa v0.12.4 h1:lhWJqcGkIIMiOYWdsoEuGlri2UbMkzMeh+VfuJPkXt4=
|
||||
github.com/diamondburned/arikawa v0.12.4/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
|
||||
github.com/diamondburned/arikawa v1.1.6 h1:Y/ioTYipS2v/NXfcAEhCnMTzrpxDjWlkjLKKcX29n6o=
|
||||
github.com/diamondburned/arikawa v1.1.6/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
|
||||
github.com/diamondburned/cchat v0.0.43 h1:HetAujSaUSdnQgAUZgprNLARjf/MSWXpCfWdvX2wOCU=
|
||||
github.com/diamondburned/cchat v0.0.43/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.45 h1:HMVSKx1h6lh2OenWaBTvMSK531hWaXAW7I0tKZepYug=
|
||||
github.com/diamondburned/cchat v0.0.45/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.46 h1:fzm2XA9uGasX0uaic1AFfUMGA53PlO+GGmkYbx49A5k=
|
||||
github.com/diamondburned/cchat v0.0.46/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.48 h1:MAzGzKY20JBh/LnirOZVPwbMq07xfqu4Lb4XsV9/sXQ=
|
||||
github.com/diamondburned/cchat v0.0.48/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
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-20200730000036-2c93cdc1974e h1:EA5Vg0x57qLURJP80XhABBW+X0sbQSh2gw5qvPbZTs4=
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200730000036-2c93cdc1974e/go.mod h1:+hSrIVYj5tIPLAorDsHj2Tbt2fWlZtOanzfEUHX53HM=
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200815223744-cdd9b6804361 h1:vx3sMBscULTnvGqjAUVWbgIQj4HGOqHQxoIljrfHC9A=
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200815223744-cdd9b6804361/go.mod h1:sW8tIqRcKux9bhMCtcqYI1fCMGCB23FoW67gcpr13fk=
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200816234747-9647ad0709f0 h1:IVL4KUyLG9i5xPGwhIeKYttWHogenKiQLgfXEnYWNvU=
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200816234747-9647ad0709f0/go.mod h1:Hl5AJstOnuP8rCdrL/Ns9W5MH09PRxQM4tG0BIMCjLw=
|
||||
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/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-20200816224505-3cd69b83a48a h1:wEldljb421/Jp84RNb0zBfqmiWt/TTQzUE6R1ap6UuQ=
|
||||
github.com/diamondburned/gotk3 v0.0.0-20200816224505-3cd69b83a48a/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 h1:OWxllHbUptXzDias6YI4MM0R3o50q8MfhkkwVIlfiNo=
|
||||
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200717072304-e483f86c08e6 h1:YN0cj0aOCa+tKmx0aD5qsbSYaIJnyrA0/+eygMKP+/w=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200717072304-e483f86c08e6/go.mod h1:Sunqp1b9Tc0+DtWKslhf83Zepgj/TELB6h8J9HZCPqQ=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200815214034-638820c48066 h1:TOLTl0zLJ+idYB7i6C4oGfmVF1Y0AFoNfVQWU3hmc4A=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200815214034-638820c48066/go.mod h1:PIsJWdDhjgN9OiR+qrDPD8KGQ8UyFuRVrgs3Ewu6a3c=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200816035718-70361fb41b6f h1:Wzns8I0VjMrcsH7N5lqHKTRkgBt2aCwx+7wAxvSFGjU=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200816035718-70361fb41b6f/go.mod h1:PIsJWdDhjgN9OiR+qrDPD8KGQ8UyFuRVrgs3Ewu6a3c=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200816040753-9595e584e6bc h1:pxFVcg7J7D9hYVVReXLdJurgv4uAo1a6jNHcYpdeZsE=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200816040753-9595e584e6bc/go.mod h1:PIsJWdDhjgN9OiR+qrDPD8KGQ8UyFuRVrgs3Ewu6a3c=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200816040956-857988325ce0 h1:c3G8NjcS7JZvQvY549r993C0P1ltEf7sFpfRD21HFhk=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200816040956-857988325ce0/go.mod h1:PIsJWdDhjgN9OiR+qrDPD8KGQ8UyFuRVrgs3Ewu6a3c=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200816192443-0b6a02d498d2 h1:PodX8lfv7ZffUHUsaQUdIjZ50ONY8uCCXXUL7yzoSMQ=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200816192443-0b6a02d498d2/go.mod h1:PIsJWdDhjgN9OiR+qrDPD8KGQ8UyFuRVrgs3Ewu6a3c=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
||||
|
@ -232,6 +252,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
|
@ -60,12 +60,6 @@ func AsyncStream(url string, fn func(r io.Reader)) {
|
|||
}
|
||||
|
||||
func get(ctx context.Context, url string, cached bool) (r *http.Response, err error) {
|
||||
// if cached {
|
||||
// r, err = dskcached.Get(url)
|
||||
// } else {
|
||||
// r, err = memcached.Get(url)
|
||||
// }
|
||||
|
||||
q, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to make a request")
|
||||
|
|
|
@ -183,14 +183,20 @@ type Avatar struct {
|
|||
}
|
||||
|
||||
func NewAvatar() *Avatar {
|
||||
avatar, _ := roundimage.NewButton()
|
||||
avatar, _ := roundimage.NewEmptyButton()
|
||||
avatar.SetSizeRequest(AvatarSize, AvatarSize)
|
||||
avatar.SetVAlign(gtk.ALIGN_START)
|
||||
|
||||
img, _ := roundimage.NewStaticImage(avatar, 0)
|
||||
img.Show()
|
||||
|
||||
avatar.SetImage(img.Image)
|
||||
|
||||
// TODO
|
||||
// Remove static image; make it internal; make static iamge bind to something else incl button and list
|
||||
|
||||
// Default icon.
|
||||
primitives.SetImageIcon(
|
||||
avatar.Image.Image, "user-available-symbolic", AvatarSize,
|
||||
)
|
||||
primitives.SetImageIcon(img, "user-available-symbolic", AvatarSize)
|
||||
|
||||
return &Avatar{*avatar, ""}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
package memberlist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"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/labeluri"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var MemberListWidth = 250
|
||||
|
||||
type Container struct {
|
||||
*gtk.ScrolledWindow
|
||||
Main *gtk.Box
|
||||
|
||||
// map id -> *Section
|
||||
Sections map[string]*Section
|
||||
|
||||
// states
|
||||
stop func()
|
||||
}
|
||||
|
||||
var memberListCSS = primitives.PrepareClassCSS("member-list", `
|
||||
.member-list {
|
||||
background-color: @theme_base_color;
|
||||
}
|
||||
`)
|
||||
|
||||
func New() *Container {
|
||||
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
||||
main.SetSizeRequest(250, -1)
|
||||
main.Show()
|
||||
memberListCSS(main)
|
||||
|
||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
sw.Add(main)
|
||||
|
||||
return &Container{
|
||||
ScrolledWindow: sw,
|
||||
Main: main,
|
||||
Sections: map[string]*Section{},
|
||||
}
|
||||
}
|
||||
|
||||
// Reset removes all old sections.
|
||||
func (c *Container) Reset() {
|
||||
if c.stop != nil {
|
||||
c.stop()
|
||||
c.stop = nil
|
||||
}
|
||||
|
||||
for _, section := range c.Sections {
|
||||
c.Main.Remove(section)
|
||||
}
|
||||
|
||||
c.Sections = map[string]*Section{}
|
||||
}
|
||||
|
||||
// TryAsyncList tries to set the member list from the given server. It does type
|
||||
// assertions and handles asynchronicity. Reset must be called before this.
|
||||
func (c *Container) TryAsyncList(server cchat.ServerMessage) {
|
||||
ls, ok := server.(cchat.ServerMessageMemberLister)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
gts.Async(func() (func(), error) {
|
||||
f, err := ls.ListMembers(context.Background(), c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to list members")
|
||||
}
|
||||
|
||||
return func() {
|
||||
c.stop = f
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) SetSections(sections []cchat.MemberListSection) {
|
||||
gts.ExecAsync(func() { c.SetSectionsUnsafe(sections) })
|
||||
}
|
||||
|
||||
func (c *Container) SetMember(sectionID string, member cchat.ListMember) {
|
||||
gts.ExecAsync(func() { c.SetMemberUnsafe(sectionID, member) })
|
||||
}
|
||||
|
||||
func (c *Container) RemoveMember(sectionID string, id string) {
|
||||
gts.ExecAsync(func() { c.RemoveMemberUnsafe(sectionID, id) })
|
||||
}
|
||||
|
||||
func (c *Container) SetSectionsUnsafe(sections []cchat.MemberListSection) {
|
||||
var newSections = make([]*Section, len(sections))
|
||||
|
||||
for i, section := range sections {
|
||||
sc, ok := c.Sections[section.ID()]
|
||||
if !ok {
|
||||
sc = NewSection(section)
|
||||
} else {
|
||||
sc.Update(section.Name(), section.Total())
|
||||
}
|
||||
|
||||
newSections[i] = sc
|
||||
}
|
||||
|
||||
// Remove all old sections.
|
||||
for id, section := range c.Sections {
|
||||
c.Main.Remove(section)
|
||||
delete(c.Sections, id)
|
||||
}
|
||||
|
||||
// Insert new sections.
|
||||
for _, section := range newSections {
|
||||
c.Main.Add(section)
|
||||
c.Sections[section.ID] = section
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) SetMemberUnsafe(sectionID string, member cchat.ListMember) {
|
||||
if s, ok := c.Sections[sectionID]; ok {
|
||||
s.SetMember(member)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) RemoveMemberUnsafe(sectionID string, id string) {
|
||||
if s, ok := c.Sections[sectionID]; ok {
|
||||
s.RemoveMember(id)
|
||||
}
|
||||
}
|
||||
|
||||
type Section struct {
|
||||
*gtk.Box
|
||||
|
||||
ID string
|
||||
|
||||
// state
|
||||
name text.Rich
|
||||
total int
|
||||
|
||||
Header *rich.Label
|
||||
Body *gtk.ListBox
|
||||
|
||||
// map id -> *Member
|
||||
Members map[string]*Member
|
||||
}
|
||||
|
||||
var sectionHeaderCSS = primitives.PrepareClassCSS("section-header", `
|
||||
.section-header {
|
||||
margin: 8px 12px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
`)
|
||||
|
||||
var sectionBodyCSS = primitives.PrepareClassCSS("section-body", `
|
||||
.section-body {
|
||||
background: inherit;
|
||||
}
|
||||
`)
|
||||
|
||||
func NewSection(sect cchat.MemberListSection) *Section {
|
||||
header := rich.NewLabel(text.Rich{})
|
||||
header.Show()
|
||||
sectionHeaderCSS(header)
|
||||
|
||||
body, _ := gtk.ListBoxNew()
|
||||
body.SetSelectionMode(gtk.SELECTION_NONE)
|
||||
body.SetActivateOnSingleClick(true)
|
||||
body.SetSortFunc(listSortNameAsc) // A-Z
|
||||
body.Show()
|
||||
sectionBodyCSS(body)
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.PackStart(header, false, false, 0)
|
||||
box.PackStart(body, false, false, 0)
|
||||
box.Show()
|
||||
|
||||
var members = map[string]*Member{}
|
||||
|
||||
// On row click, show the mention popup if any.
|
||||
body.Connect("row-activated", func(_ *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||
var i = r.GetIndex()
|
||||
// Cold path; we can afford searching in the map.
|
||||
for _, member := range members {
|
||||
if member.ListBoxRow.GetIndex() == i {
|
||||
member.Popup()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
section := &Section{
|
||||
ID: sect.ID(),
|
||||
Box: box,
|
||||
Header: header,
|
||||
Body: body,
|
||||
Members: members,
|
||||
}
|
||||
|
||||
section.Update(sect.Name(), sect.Total())
|
||||
|
||||
return section
|
||||
}
|
||||
|
||||
func (s *Section) Update(name text.Rich, total int) {
|
||||
s.name = name
|
||||
s.total = total
|
||||
|
||||
var content = s.name.Content
|
||||
if total > 0 {
|
||||
content += fmt.Sprintf("—%d", total)
|
||||
}
|
||||
|
||||
s.Header.SetLabelUnsafe(text.Rich{
|
||||
Content: content,
|
||||
Segments: s.name.Segments,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Section) SetMember(member cchat.ListMember) {
|
||||
if m, ok := s.Members[member.ID()]; ok {
|
||||
m.Update(member)
|
||||
return
|
||||
}
|
||||
|
||||
m := NewMember(member)
|
||||
m.Show()
|
||||
|
||||
s.Members[member.ID()] = m
|
||||
s.Body.Add(m)
|
||||
}
|
||||
|
||||
func (s *Section) RemoveMember(id string) {
|
||||
if member, ok := s.Members[id]; ok {
|
||||
s.Body.Remove(member)
|
||||
delete(s.Members, id)
|
||||
}
|
||||
}
|
||||
|
||||
func listSortNameAsc(r1, r2 *gtk.ListBoxRow, _ ...interface{}) int {
|
||||
n1, _ := r1.GetName()
|
||||
n2, _ := r2.GetName()
|
||||
|
||||
switch {
|
||||
case n1 < n2:
|
||||
return -1
|
||||
case n1 > n2:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
*gtk.ListBoxRow
|
||||
Main *gtk.Box
|
||||
|
||||
Avatar *rich.Icon
|
||||
Name *gtk.Label
|
||||
output markup.RenderOutput
|
||||
}
|
||||
|
||||
const AvatarSize = 34
|
||||
|
||||
var memberRowCSS = primitives.PrepareClassCSS("member-row", `
|
||||
.member-row {
|
||||
min-height: 42px;
|
||||
}
|
||||
`)
|
||||
|
||||
var memberBoxCSS = primitives.PrepareClassCSS("member-box", `
|
||||
.member-box {
|
||||
margin: 3px 10px;
|
||||
}
|
||||
`)
|
||||
|
||||
var avatarMemberCSS = primitives.PrepareClassCSS("avatar-member", `
|
||||
.avatar-member {
|
||||
padding-right: 10px;
|
||||
}
|
||||
`)
|
||||
|
||||
func NewMember(member cchat.ListMember) *Member {
|
||||
evb, _ := gtk.EventBoxNew()
|
||||
evb.AddEvents(int(gdk.EVENT_ENTER_NOTIFY) | int(gdk.EVENT_LEAVE_NOTIFY))
|
||||
evb.Show()
|
||||
|
||||
img, _ := roundimage.NewStaticImage(evb, 0)
|
||||
img.Show()
|
||||
|
||||
icon := rich.NewCustomIcon(img, AvatarSize)
|
||||
icon.SetPlaceholderIcon("user-info-symbolic", AvatarSize)
|
||||
icon.Show()
|
||||
avatarMemberCSS(icon)
|
||||
|
||||
lbl, _ := gtk.LabelNew("")
|
||||
lbl.SetUseMarkup(true)
|
||||
lbl.SetXAlign(0)
|
||||
lbl.SetEllipsize(pango.ELLIPSIZE_END)
|
||||
lbl.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.PackStart(icon, false, false, 0)
|
||||
box.PackStart(lbl, true, true, 0)
|
||||
box.Show()
|
||||
memberBoxCSS(box)
|
||||
|
||||
evb.Add(box)
|
||||
|
||||
r, _ := gtk.ListBoxRowNew()
|
||||
memberRowCSS(r)
|
||||
r.Add(evb)
|
||||
|
||||
m := &Member{
|
||||
ListBoxRow: r,
|
||||
Main: box,
|
||||
Avatar: icon,
|
||||
Name: lbl,
|
||||
}
|
||||
|
||||
m.Update(member)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Member) Update(member cchat.ListMember) {
|
||||
m.ListBoxRow.SetName(member.Name().Content)
|
||||
|
||||
if iconer, ok := member.(cchat.Icon); ok {
|
||||
m.Avatar.AsyncSetIconer(iconer, "Failed to get member list icon")
|
||||
}
|
||||
|
||||
m.output = markup.RenderCmplxWithConfig(member.Name(), markup.NoMentionLinks)
|
||||
txt := strings.Builder{}
|
||||
txt.WriteString(fmt.Sprintf(
|
||||
`<span color="#%06X">●</span> %s`,
|
||||
statusColors(member.Status()), m.output.Markup,
|
||||
))
|
||||
|
||||
if bot := member.Secondary(); !bot.Empty() {
|
||||
txt.WriteByte('\n')
|
||||
txt.WriteString(fmt.Sprintf(
|
||||
`<span alpha="85%%"><sup>%s</sup></span>`,
|
||||
markup.Render(bot),
|
||||
))
|
||||
}
|
||||
|
||||
m.Name.SetMarkup(txt.String())
|
||||
}
|
||||
|
||||
// Popup pops up the mention popover if any.
|
||||
func (m *Member) Popup() {
|
||||
if len(m.output.Mentions) > 0 {
|
||||
p := labeluri.NewPopoverMentioner(m, m.output.Input, m.output.Mentions[0])
|
||||
p.SetPosition(gtk.POS_LEFT)
|
||||
p.Popup()
|
||||
}
|
||||
}
|
||||
|
||||
func statusColors(status cchat.UserStatus) uint32 {
|
||||
switch status {
|
||||
case cchat.OnlineStatus:
|
||||
return 0x43B581
|
||||
case cchat.BusyStatus:
|
||||
return 0xF04747
|
||||
case cchat.IdleStatus:
|
||||
return 0xFAA61A
|
||||
case cchat.OfflineStatus:
|
||||
fallthrough
|
||||
default:
|
||||
return 0x747F8D
|
||||
}
|
||||
}
|
|
@ -190,14 +190,8 @@ func (m *GenericContainer) UpdateAuthor(author cchat.MessageAuthor) {
|
|||
}
|
||||
}
|
||||
|
||||
// authorRenderCfg is the config to render author names. It disables author
|
||||
// mention links, as there's no way to make normal names not appear blue.
|
||||
var authorRenderCfg = markup.RenderConfig{
|
||||
NoMentionLinks: true,
|
||||
}
|
||||
|
||||
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
|
||||
var out = markup.RenderCmplxWithConfig(name, authorRenderCfg)
|
||||
var out = markup.RenderCmplxWithConfig(name, markup.NoMentionLinks)
|
||||
m.Username.SetOutput(out)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/compact"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/cozy"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/memberlist"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/sadface"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/typing"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
|
@ -39,7 +40,7 @@ func init() {
|
|||
|
||||
type View struct {
|
||||
*sadface.FaceView
|
||||
Box *gtk.Box
|
||||
Grid *gtk.Grid
|
||||
|
||||
Scroller *autoscroll.ScrolledWindow
|
||||
InputView *input.InputView
|
||||
|
@ -49,6 +50,8 @@ type View struct {
|
|||
Container container.Container
|
||||
contType int // msgIndex
|
||||
|
||||
MemberList *memberlist.Container
|
||||
|
||||
// Inherit some useful methods.
|
||||
state
|
||||
}
|
||||
|
@ -58,6 +61,9 @@ func NewView() *View {
|
|||
view.Typing = typing.New()
|
||||
view.Typing.Show()
|
||||
|
||||
view.MemberList = memberlist.New()
|
||||
view.MemberList.Show()
|
||||
|
||||
view.MsgBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
||||
view.MsgBox.PackEnd(view.Typing, false, false, 0)
|
||||
view.MsgBox.Show()
|
||||
|
@ -68,31 +74,36 @@ func NewView() *View {
|
|||
|
||||
view.Scroller = autoscroll.NewScrolledWindow()
|
||||
view.Scroller.Add(view.MsgBox)
|
||||
view.Scroller.SetVExpand(true)
|
||||
view.Scroller.SetHExpand(true)
|
||||
view.Scroller.Show()
|
||||
|
||||
// A separator to go inbetween.
|
||||
sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
|
||||
sep.SetHExpand(true)
|
||||
sep.Show()
|
||||
|
||||
view.InputView = input.NewView(view)
|
||||
view.InputView.SetHExpand(true)
|
||||
view.InputView.Show()
|
||||
|
||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
view.Box.PackStart(view.Scroller, true, true, 0)
|
||||
view.Box.PackStart(sep, false, false, 0)
|
||||
view.Box.PackStart(view.InputView, false, false, 0)
|
||||
view.Box.Show()
|
||||
view.Grid, _ = gtk.GridNew()
|
||||
view.Grid.Attach(view.Scroller, 0, 0, 1, 1)
|
||||
view.Grid.Attach(sep, 0, 1, 1, 1)
|
||||
view.Grid.Attach(view.InputView, 0, 2, 1, 1)
|
||||
view.Grid.Attach(view.MemberList, 1, 0, 1, 3)
|
||||
view.Grid.Show()
|
||||
|
||||
primitives.AddClass(view.Box, "message-view")
|
||||
primitives.AddClass(view.Grid, "message-view")
|
||||
|
||||
// Bind a file drag-and-drop box into the main view box.
|
||||
drag.BindFileDest(view.Box, view.InputView.Attachments.AddFiles)
|
||||
drag.BindFileDest(view.Grid, view.InputView.Attachments.AddFiles)
|
||||
|
||||
// placeholder logo
|
||||
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2(128))
|
||||
logo.Show()
|
||||
|
||||
view.FaceView = sadface.New(view.Box, logo)
|
||||
view.FaceView = sadface.New(view.Grid, logo)
|
||||
return view
|
||||
}
|
||||
|
||||
|
@ -117,11 +128,12 @@ func (v *View) createMessageContainer() {
|
|||
func (v *View) Bottomed() bool { return v.Scroller.Bottomed }
|
||||
|
||||
func (v *View) Reset() {
|
||||
v.state.Reset() // Reset the state variables.
|
||||
v.Typing.Reset() // Reset the typing state.
|
||||
v.InputView.Reset() // Reset the input.
|
||||
v.Container.Reset() // Clean all messages.
|
||||
v.FaceView.Reset() // Switch back to the main screen.
|
||||
v.state.Reset() // Reset the state variables.
|
||||
v.Typing.Reset() // Reset the typing state.
|
||||
v.InputView.Reset() // Reset the input.
|
||||
v.MemberList.Reset() // Reset the member list.
|
||||
v.Container.Reset() // Clean all messages.
|
||||
v.FaceView.Reset() // Switch back to the main screen.
|
||||
|
||||
// Keep the scroller at the bottom.
|
||||
v.Scroller.Bottomed = true
|
||||
|
@ -174,6 +186,9 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func
|
|||
|
||||
// Try setting the typing indicator if available.
|
||||
v.Typing.TrySubscribe(server)
|
||||
|
||||
// Try and use the list.
|
||||
v.MemberList.TryAsyncList(server)
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package primitives
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
|
@ -110,7 +109,12 @@ func NewImageIconPx(icon string, sizepx int) *gtk.Image {
|
|||
return img
|
||||
}
|
||||
|
||||
func SetImageIcon(img *gtk.Image, icon string, sizepx int) {
|
||||
type ImageIconSetter interface {
|
||||
SetProperty(name string, value interface{}) error
|
||||
SetSizeRequest(w, h int)
|
||||
}
|
||||
|
||||
func SetImageIcon(img ImageIconSetter, icon string, sizepx int) {
|
||||
img.SetProperty("icon-name", icon)
|
||||
img.SetProperty("pixel-size", sizepx)
|
||||
img.SetSizeRequest(sizepx, sizepx)
|
||||
|
@ -244,9 +248,7 @@ func PrepareClassCSS(class, css string) (attach func(StyleContexter)) {
|
|||
func PrepareCSS(css string) *gtk.CssProvider {
|
||||
p, _ := gtk.CssProviderNew()
|
||||
if err := p.LoadFromData(css); err != nil {
|
||||
_, fn, caller, _ := runtime.Caller(1)
|
||||
fn = filepath.Base(fn)
|
||||
log.Error(errors.Wrapf(err, "CSS fail at %s:%d", fn, caller))
|
||||
log.Error(errors.Wrapf(err, "CSS fail at %s", debug.Stack()))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
|
|
@ -3,8 +3,10 @@ package roundimage
|
|||
import (
|
||||
"math"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/gotk3/gotk3/cairo"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
|
@ -31,12 +33,76 @@ func NewButton() (*Button, error) {
|
|||
image, _ := NewImage(0)
|
||||
image.Show()
|
||||
|
||||
b, _ := gtk.ButtonNew()
|
||||
b, _ := NewEmptyButton()
|
||||
b.SetImage(image)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func NewEmptyButton() (*Button, error) {
|
||||
b, _ := gtk.ButtonNew()
|
||||
b.SetRelief(gtk.RELIEF_NONE)
|
||||
roundButtonCSS(b)
|
||||
|
||||
return &Button{Button: b, Image: image}, nil
|
||||
return &Button{Button: b}, nil
|
||||
}
|
||||
|
||||
func (b *Button) SetImage(img *Image) {
|
||||
b.Image = img
|
||||
b.Button.SetImage(img)
|
||||
}
|
||||
|
||||
type RadiusSetter interface {
|
||||
SetRadius(float64)
|
||||
}
|
||||
|
||||
// StaticImage is an image that only plays a GIF if it's hovered on top of.
|
||||
type StaticImage struct {
|
||||
*Image
|
||||
animation *gdk.PixbufAnimation
|
||||
}
|
||||
|
||||
var _ httputil.ImageContainer = (*StaticImage)(nil)
|
||||
|
||||
func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage, error) {
|
||||
i, err := NewImage(radius)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var s = &StaticImage{i, nil}
|
||||
if parent != nil {
|
||||
s.ConnectHandlers(parent)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
|
||||
connector.Connect("enter-notify-event", func() {
|
||||
if s.animation != nil {
|
||||
s.Image.SetFromAnimation(s.animation)
|
||||
}
|
||||
})
|
||||
connector.Connect("leave-notify-event", func() {
|
||||
if s.animation != nil {
|
||||
s.Image.SetFromPixbuf(s.animation.GetStaticImage())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||
s.animation = nil
|
||||
s.Image.SetFromPixbuf(pb)
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetFromAnimation(anim *gdk.PixbufAnimation) {
|
||||
s.animation = anim
|
||||
s.Image.SetFromPixbuf(anim.GetStaticImage())
|
||||
}
|
||||
|
||||
func (s *StaticImage) GetAnimation() *gdk.PixbufAnimation {
|
||||
return s.animation
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
|
|
|
@ -10,16 +10,33 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/imgutil"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type IconerFn = func(context.Context, cchat.IconContainer) (func(), error)
|
||||
|
||||
type RoundIconContainer interface {
|
||||
gtk.IWidget
|
||||
httputil.ImageContainer
|
||||
primitives.ImageIconSetter
|
||||
roundimage.RadiusSetter
|
||||
|
||||
GetStorageType() gtk.ImageType
|
||||
GetPixbuf() *gdk.Pixbuf
|
||||
GetAnimation() *gdk.PixbufAnimation
|
||||
}
|
||||
|
||||
var (
|
||||
_ RoundIconContainer = (*roundimage.Image)(nil)
|
||||
_ RoundIconContainer = (*roundimage.StaticImage)(nil)
|
||||
)
|
||||
|
||||
// Icon represents a rounded image container.
|
||||
type Icon struct {
|
||||
*gtk.Revealer
|
||||
Image *roundimage.Image
|
||||
Image RoundIconContainer
|
||||
procs []imgutil.Processor
|
||||
size int
|
||||
|
||||
|
@ -34,13 +51,16 @@ const DefaultIconSize = 16
|
|||
var _ cchat.IconContainer = (*Icon)(nil)
|
||||
|
||||
func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
|
||||
img, _ := roundimage.NewImage(0)
|
||||
img.Show()
|
||||
return NewCustomIcon(img, sizepx, procs...)
|
||||
}
|
||||
|
||||
func NewCustomIcon(img RoundIconContainer, sizepx int, procs ...imgutil.Processor) *Icon {
|
||||
if sizepx == 0 {
|
||||
sizepx = DefaultIconSize
|
||||
}
|
||||
|
||||
img, _ := roundimage.NewImage(0)
|
||||
img.Show()
|
||||
|
||||
rev, _ := gtk.RevealerNew()
|
||||
rev.Add(img)
|
||||
rev.SetRevealChild(false)
|
||||
|
@ -94,7 +114,7 @@ func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
|
|||
i.SetRevealChild(true)
|
||||
|
||||
if iconName != "" {
|
||||
primitives.SetImageIcon(i.Image.Image, iconName, iconSzPx)
|
||||
primitives.SetImageIcon(i.Image, iconName, iconSzPx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,7 +192,9 @@ func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
|||
l := NewLabel(content)
|
||||
l.Show()
|
||||
|
||||
i := NewIcon(0)
|
||||
img, _ := roundimage.NewStaticImage(nil, 0)
|
||||
img.Show()
|
||||
i := NewCustomIcon(img, 0)
|
||||
i.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
|
@ -183,6 +205,8 @@ func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
|||
b, _ := gtk.ToggleButtonNew()
|
||||
b.Add(box)
|
||||
|
||||
img.ConnectHandlers(b)
|
||||
|
||||
return &ToggleButtonImage{
|
||||
ToggleButton: *b,
|
||||
Labeler: l, // easy inheritance of methods
|
||||
|
|
|
@ -91,7 +91,7 @@ func BindRichLabel(label Labeler) {
|
|||
var output = label.Output()
|
||||
|
||||
if mention := output.IsMention(uri); mention != nil {
|
||||
if p := popoverMentioner(label, output.Input, mention); p != nil {
|
||||
if p := NewPopoverMentioner(label, output.Input, mention); p != nil {
|
||||
p.SetPointingTo(ptr)
|
||||
p.Popup()
|
||||
}
|
||||
|
@ -104,12 +104,12 @@ func BindRichLabel(label Labeler) {
|
|||
}
|
||||
|
||||
func PopoverMentioner(rel gtk.IWidget, input string, mention text.Mentioner) {
|
||||
if p := popoverMentioner(rel, input, mention); p != nil {
|
||||
if p := NewPopoverMentioner(rel, input, mention); p != nil {
|
||||
p.Popup()
|
||||
}
|
||||
}
|
||||
|
||||
func popoverMentioner(rel gtk.IWidget, input string, mention text.Mentioner) *gtk.Popover {
|
||||
func NewPopoverMentioner(rel gtk.IWidget, input string, mention text.Mentioner) *gtk.Popover {
|
||||
var info = mention.MentionInfo()
|
||||
if info.Empty() {
|
||||
return nil
|
||||
|
|
|
@ -61,6 +61,12 @@ type RenderConfig struct {
|
|||
NoMentionLinks bool
|
||||
}
|
||||
|
||||
// NoMentionLinks is the config to render author names. It disables author
|
||||
// mention links, as there's no way to make normal names not appear blue.
|
||||
var NoMentionLinks = RenderConfig{
|
||||
NoMentionLinks: true,
|
||||
}
|
||||
|
||||
func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
|
||||
// Fast path.
|
||||
if len(content.Segments) == 0 {
|
||||
|
|
|
@ -71,7 +71,9 @@ type Row struct {
|
|||
unreadClass primitives.ClassEnum
|
||||
}
|
||||
|
||||
var rowCSS = primitives.PrepareClassCSS("session-row", `
|
||||
var rowCSS = primitives.PrepareClassCSS("session-row",
|
||||
button.UnreadColorDefs+`
|
||||
|
||||
.session-row:last-child {
|
||||
border-radius: 0 0 14px 14px;
|
||||
}
|
||||
|
@ -103,8 +105,7 @@ var rowCSS = primitives.PrepareClassCSS("session-row", `
|
|||
0.65,
|
||||
), 0.85);
|
||||
}
|
||||
|
||||
`+button.UnreadColorDefs)
|
||||
`)
|
||||
|
||||
var rowIconCSS = primitives.PrepareClassCSS("session-icon", `
|
||||
.session-icon {
|
||||
|
|
Loading…
Reference in New Issue