Refactor server browser (message list not working)

This commit is contained in:
diamondburned 2021-03-29 14:46:52 -07:00
parent b822f63c18
commit e73f9a099b
60 changed files with 2275 additions and 1967 deletions

18
go.mod
View File

@ -1,26 +1,20 @@
module github.com/diamondburned/cchat-gtk module github.com/diamondburned/cchat-gtk
go 1.14 go 1.16
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20210223232102-0ce0ad615fb2 replace github.com/diamondburned/cchat-discord => ../cchat-discord
// replace github.com/diamondburned/cchat-discord => ../cchat-discord
// replace github.com/diamondburned/gotk3-tcmalloc => ../../gotk3-tcmalloc
// replace github.com/diamondburned/ningen/v2 => ../../ningen
// replace github.com/diamondburned/arikawa/v2 => ../../arikawa
require ( require (
github.com/Xuanwo/go-locale v1.0.0 github.com/Xuanwo/go-locale v1.0.0
github.com/alecthomas/chroma v0.7.3 github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.3.17 github.com/diamondburned/cchat v0.6.4
github.com/diamondburned/cchat-discord v0.0.0-20210107014523-4fefdf1b9332 github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374 github.com/diamondburned/handy v0.0.0-20210329054445-387ad28eb2c2
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/goodsign/monday v1.0.0 github.com/goodsign/monday v1.0.0
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194 github.com/gotk3/gotk3 v0.5.3-0.20210326060404-6328e5470ece
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
github.com/peterbourgon/diskv v2.0.1+incompatible github.com/peterbourgon/diskv v2.0.1+incompatible
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1

101
go.sum
View File

@ -6,16 +6,24 @@ cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxK
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0 h1:MZQCQQaRwOrAcuKjiHWHrgKykt4fZyuwF2dtiG3fGW8=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0 h1:sAbMqjY1PEQKZBWfbu6Y6bsupJ9c4QdHnzg/VvYTLcE=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0 h1:9/vpR43S4aJaROxqQHQ3nH9lfyKKV0dC3vOmnw8ebQQ=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0 h1:RPUcBvDeYgQFMfQu1eBMq6piD1SXmLH+vK3qjewZPus=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg= github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg=
github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y= github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
@ -27,19 +35,26 @@ github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMA
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4 h1:Y0ZBCHAvHhTHw7FFJ2FzCAAG4pkbTgA45nc7BpMhDNk=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU=
github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/dave/jennifer v1.4.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= github.com/dave/jennifer v1.4.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
github.com/dave/jennifer v1.4.1 h1:XyqG6cn5RQsTj3qlWQTKlRGAyrTcsk1kUmWdZBzRjDw=
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -60,12 +75,28 @@ github.com/diamondburned/arikawa/v2 v2.0.0-20210105213913-8a213759164c h1:6n1EqF
github.com/diamondburned/arikawa/v2 v2.0.0-20210105213913-8a213759164c/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0= github.com/diamondburned/arikawa/v2 v2.0.0-20210105213913-8a213759164c/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0=
github.com/diamondburned/arikawa/v2 v2.0.0-20210106050916-771591e5eb65 h1:foJMpT+BAoASVzDj9WDxNp6/OxnWnQ/uUHk2DXARP/Y= github.com/diamondburned/arikawa/v2 v2.0.0-20210106050916-771591e5eb65 h1:foJMpT+BAoASVzDj9WDxNp6/OxnWnQ/uUHk2DXARP/Y=
github.com/diamondburned/arikawa/v2 v2.0.0-20210106050916-771591e5eb65/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0= github.com/diamondburned/arikawa/v2 v2.0.0-20210106050916-771591e5eb65/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0=
github.com/diamondburned/arikawa/v2 v2.0.5 h1:X/jPfeEFj/Hzk26kudNJqtVkQb3vnh3tAjEF1mY7GK8=
github.com/diamondburned/arikawa/v2 v2.0.5/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0=
github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg= github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg=
github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.15 h1:BJf8ZiRtDWTGMtQ3QqjNU0H+784WSrkJEpFGkKY5gEw= github.com/diamondburned/cchat v0.3.15 h1:BJf8ZiRtDWTGMtQ3QqjNU0H+784WSrkJEpFGkKY5gEw=
github.com/diamondburned/cchat v0.3.15/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= github.com/diamondburned/cchat v0.3.15/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.17 h1:pGwas8Y0SBU7yg4EQ/MvrbqZhrnRhPBYm1AiRsL147s= github.com/diamondburned/cchat v0.3.17 h1:pGwas8Y0SBU7yg4EQ/MvrbqZhrnRhPBYm1AiRsL147s=
github.com/diamondburned/cchat v0.3.17/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= github.com/diamondburned/cchat v0.3.17/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.5.4 h1:0N/H+zs4T6YBOP5dtUnJ+paZfq06772b5CXRw7bWx7U=
github.com/diamondburned/cchat v0.5.4/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.5.5 h1:YgI98ID9UQAgHbLFEyHQ2qcAJ5A2HcVZaq+A26njAQo=
github.com/diamondburned/cchat v0.5.5/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.5.6 h1:U6f41fEtXgs+fdmk0q9ouQsfzY7nQzp9N8fV0FfEbv0=
github.com/diamondburned/cchat v0.5.6/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.6.0 h1:XVql8slOho9FUxuJm/I7/XWr5l1XgxLc5SvcvI+ybp4=
github.com/diamondburned/cchat v0.6.0/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.6.1 h1:BFpY2Nvnut1/X+0dhL8STK80RVmfEJLmf4ppTJX2ADg=
github.com/diamondburned/cchat v0.6.1/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.6.2 h1:H0Hyd2irpKlTR03ulm6IMy7RVorPtvQS5X0kTn26Up8=
github.com/diamondburned/cchat v0.6.2/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.6.4 h1:CFh6eFNE+SQ7IOjb1QDcgTgv/I7W2k6E7d3n8qJ+ZrQ=
github.com/diamondburned/cchat v0.6.4/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d h1:n61DxLdX7nPj7KA1N/azaR8wa0pnDBDT6Yi1seOsBWM= github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d h1:n61DxLdX7nPj7KA1N/azaR8wa0pnDBDT6Yi1seOsBWM=
github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms= github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af h1:pTdxsrVSYCdraGormbu1t8uQJMe/OD/ZIz9KljDWAvc= github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af h1:pTdxsrVSYCdraGormbu1t8uQJMe/OD/ZIz9KljDWAvc=
@ -98,6 +129,22 @@ github.com/diamondburned/cchat-discord v0.0.0-20210107010729-8edbbcc24992 h1:njL
github.com/diamondburned/cchat-discord v0.0.0-20210107010729-8edbbcc24992/go.mod h1:g+RqSLt/ccZZVZbsukOiCwQCqKM/9HfHMJVboIUW+tU= github.com/diamondburned/cchat-discord v0.0.0-20210107010729-8edbbcc24992/go.mod h1:g+RqSLt/ccZZVZbsukOiCwQCqKM/9HfHMJVboIUW+tU=
github.com/diamondburned/cchat-discord v0.0.0-20210107014523-4fefdf1b9332 h1:nGOfPHgujDdvltCkzMiKhldlo9qLyflT8weG+xWOi2E= github.com/diamondburned/cchat-discord v0.0.0-20210107014523-4fefdf1b9332 h1:nGOfPHgujDdvltCkzMiKhldlo9qLyflT8weG+xWOi2E=
github.com/diamondburned/cchat-discord v0.0.0-20210107014523-4fefdf1b9332/go.mod h1:g+RqSLt/ccZZVZbsukOiCwQCqKM/9HfHMJVboIUW+tU= github.com/diamondburned/cchat-discord v0.0.0-20210107014523-4fefdf1b9332/go.mod h1:g+RqSLt/ccZZVZbsukOiCwQCqKM/9HfHMJVboIUW+tU=
github.com/diamondburned/cchat-discord v0.0.0-20210318171231-71f2e1570400 h1:RwU9SqiWT/iqrosB0Nz54t1CRjMTP/XhZYUIT+jirgE=
github.com/diamondburned/cchat-discord v0.0.0-20210318171231-71f2e1570400/go.mod h1:OoF9yCU2eZmC8fNh7rJ+xaANLPsH8EWzFR7QG13UWLM=
github.com/diamondburned/cchat-discord v0.0.0-20210318194307-9dc28023dac5 h1:Zh3qkQmJowAdE6G8/hQJ8TLnTo03IDbCW9Lq/mIV4Ko=
github.com/diamondburned/cchat-discord v0.0.0-20210318194307-9dc28023dac5/go.mod h1:hkAXdLdcTYb87ZTBvfEopEavbs8x7xPygJGpK5Z2Es8=
github.com/diamondburned/cchat-discord v0.0.0-20210318212724-ba43f691bccc h1:sRXWos0PGQ7JrASu35AnF6tsT0wN3ZC9r5OTVNusr5k=
github.com/diamondburned/cchat-discord v0.0.0-20210318212724-ba43f691bccc/go.mod h1:ZFzQ34q1tr3ci5HRyqzbmoth7HMsXO9iTXImc112Yeg=
github.com/diamondburned/cchat-discord v0.0.0-20210320000032-f3e0d38c382d h1:tBcY8AqCXDq+hicxQGuFj8HTWt+3YnjfinexNbniWSA=
github.com/diamondburned/cchat-discord v0.0.0-20210320000032-f3e0d38c382d/go.mod h1:H5UuKeE097pqWKvWwdwEBIByO6chZgTva1qsiE8Jm4Q=
github.com/diamondburned/cchat-discord v0.0.0-20210320051719-18eb6081460f h1:alREboGThGvNzYFq+bqdhrntYvpClvkMAsnc3xblMZU=
github.com/diamondburned/cchat-discord v0.0.0-20210320051719-18eb6081460f/go.mod h1:yn0CA0/JyIs3U8v+B487DQWg2037ePvqcTZS1CZQGVA=
github.com/diamondburned/cchat-discord v0.0.0-20210325233237-f9814bf4101b h1:l35x20NdcMWnEbGyNSS0K+PDXbTclCMCK2n0hU8pV/U=
github.com/diamondburned/cchat-discord v0.0.0-20210325233237-f9814bf4101b/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
github.com/diamondburned/cchat-discord v0.0.0-20210326063215-9eb392a95413 h1:r6PAQX0PhUXVB7Uuxd9BNNOvNXBs73DUIlNSjDXHqFk=
github.com/diamondburned/cchat-discord v0.0.0-20210326063215-9eb392a95413/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff h1:p5XYPavnJ89wrJAf4ns6f1OfHQz5NMU9uXlX3EiKdfU=
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg=
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw= github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=
@ -138,6 +185,10 @@ github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4 h1:qF5VHC35+Gy
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4/go.mod h1:V0qyhW4v6KPFwtDpXdBm5aWH7zWEyrzZpcB6MPnKArQ= github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4/go.mod h1:V0qyhW4v6KPFwtDpXdBm5aWH7zWEyrzZpcB6MPnKArQ=
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374 h1:1KLPz5mbYF7t3ajrK55alkDcbjWc+7aQFjKzV9qJRN4= github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374 h1:1KLPz5mbYF7t3ajrK55alkDcbjWc+7aQFjKzV9qJRN4=
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374/go.mod h1:9EiMOAKWhEFoVnFLTco2v0v0rJn855YAHDDa2bL8urU= github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374/go.mod h1:9EiMOAKWhEFoVnFLTco2v0v0rJn855YAHDDa2bL8urU=
github.com/diamondburned/handy v0.0.0-20210317194712-ec07079c938b h1:vcu2dF4RyJM+w3nRgNsbOnGjdf/Bxd5etjyNJJ8trvY=
github.com/diamondburned/handy v0.0.0-20210317194712-ec07079c938b/go.mod h1:WsMP6/3+U5b8ZmIn7MilCXHhhph1uvqkBRr86Fubm+k=
github.com/diamondburned/handy v0.0.0-20210329054445-387ad28eb2c2 h1:fIQVMaOTWAcv6nThIyzKuyAO/GccwtVYDovnEpFFXGs=
github.com/diamondburned/handy v0.0.0-20210329054445-387ad28eb2c2/go.mod h1:+edD3Anr1+QF0HjgOUFJRkPiVAt0mj3IczvZc+yIuvo=
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 h1:OWxllHbUptXzDias6YI4MM0R3o50q8MfhkkwVIlfiNo= 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/imgutil v0.0.0-20200710174014-8a3be144a972/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a h1:w8CWPYiwH9p2XGlHHeTqRWx7e8CJJLN8i4orAkOa27Y= github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a h1:w8CWPYiwH9p2XGlHHeTqRWx7e8CJJLN8i4orAkOa27Y=
@ -160,25 +211,33 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
github.com/go-test/deep v1.0.7/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 h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
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=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc= github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8= github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8=
@ -188,13 +247,18 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12 h1:TgXhFz35pKlZuUz1pNlOKk1UCSXPpuUIc144Wd7SxCA=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -202,12 +266,21 @@ github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotk3/gotk3 v0.4.0/go.mod h1:Eew3QBwAOBTrfFFDmsDE5wZWbcagBL1NUslj1GhRveo=
github.com/gotk3/gotk3 v0.4.1-0.20200321173312-c4ae30c61acd/go.mod h1:Eew3QBwAOBTrfFFDmsDE5wZWbcagBL1NUslj1GhRveo=
github.com/gotk3/gotk3 v0.5.3-0.20210311170413-be85685ca6db h1:6DrB5/BH3GK8Wg6VZ+C2NqQQdMof0iHifNqxixBKefk=
github.com/gotk3/gotk3 v0.5.3-0.20210311170413-be85685ca6db/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/gotk3/gotk3 v0.5.3-0.20210326060404-6328e5470ece h1:97BTiWMRSBA/fkg4irBHzQPv8E30dIUThuAtQn/MK6Q=
github.com/gotk3/gotk3 v0.5.3-0.20210326060404-6328e5470ece/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
@ -215,9 +288,11 @@ github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
github.com/kawasin73/umutex v0.2.1 h1:Onkzz3LKs1HThskVwdhhBocqdRQqwCZ03quDJzuPzPo= github.com/kawasin73/umutex v0.2.1 h1:Onkzz3LKs1HThskVwdhhBocqdRQqwCZ03quDJzuPzPo=
github.com/kawasin73/umutex v0.2.1/go.mod h1:A02N2muKVFMvFlp5c+hBycgdH964YtieGs+7mYB16NU= github.com/kawasin73/umutex v0.2.1/go.mod h1:A02N2muKVFMvFlp5c+hBycgdH964YtieGs+7mYB16NU=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@ -225,11 +300,13 @@ github.com/lithammer/fuzzysearch v1.1.1 h1:8F9OAV2xPuYblToVohjanztdnPjbtA0MLgMvD
github.com/lithammer/fuzzysearch v1.1.1/go.mod h1:H2bng+w5gsR7NlfIJM8ElGZI0sX6C/9uzGqicVXGU6c= github.com/lithammer/fuzzysearch v1.1.1/go.mod h1:H2bng+w5gsR7NlfIJM8ElGZI0sX6C/9uzGqicVXGU6c=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@ -238,8 +315,11 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@ -261,11 +341,14 @@ github.com/twmb/murmur3 v1.1.3 h1:D83U0XYKcHRYwYIpBKf3Pks91Z0Byda/9SJ8B6EMRcA=
github.com/twmb/murmur3 v1.1.3/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/twmb/murmur3 v1.1.3/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/yuin/goldmark v1.1.30 h1:j4d4Lw3zqZelDhBksEo3BnWg9xhXRQGJPPSL6OApZjI= github.com/yuin/goldmark v1.1.30 h1:j4d4Lw3zqZelDhBksEo3BnWg9xhXRQGJPPSL6OApZjI=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.2 h1:YjHC5TgyMmHpicTgEqDN0Q96Xo8K6tLXPnmNOHXCgs0=
github.com/yuin/goldmark v1.3.2/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717 h1:3M/uUZajYn/082wzUajekePxpUAZhMTfXvI9R+26SJ0= github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717 h1:3M/uUZajYn/082wzUajekePxpUAZhMTfXvI9R+26SJ0=
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0= github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
@ -273,6 +356,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -281,6 +365,7 @@ golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm0
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd h1:zkO/Lhoka23X63N9OSzpSeROEUQ5ODw47tM3YWjygbs=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -294,12 +379,15 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -314,11 +402,13 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -383,9 +473,11 @@ golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56 h1:DFtSed2q3HtNuVazwVDZ4nSRS/JrZEig0gz2BY4VNrg=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -394,11 +486,13 @@ google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEn
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0 h1:0q95w+VuFtv4PAx4PZVQdBMmYbaCHbnfKaEiDIcVyag=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -412,6 +506,7 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce h1:1mbrb1tUU+Zmt5C94IGKADBTJZjZXAd+BubWi7r9EiI=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@ -419,11 +514,13 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -433,7 +530,11 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,8 @@ package icons
import ( import (
"log" "log"
_ "embed"
"github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/cairo"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
) )
@ -10,6 +12,12 @@ import (
// static assets // static assets
// var assets = map[string]*gdk.Pixbuf{} // var assets = map[string]*gdk.Pixbuf{}
//go:embed cchat_256.png
var __cchat_256 []byte
//go:embed cchat-variant2_256.png
var __cchat_variant2_256 []byte
func Logo256Variant2(sz, scale int) *cairo.Surface { func Logo256Variant2(sz, scale int) *cairo.Surface {
return mustSurface(loadPixbuf(__cchat_variant2_256, sz, scale), scale) return mustSurface(loadPixbuf(__cchat_variant2_256, sz, scale), scale)
} }

View File

@ -1,7 +1,6 @@
package httputil package httputil
import ( import (
"bufio"
"context" "context"
"io" "io"
"mime" "mime"
@ -9,7 +8,6 @@ import (
"net/url" "net/url"
"path" "path"
"strings" "strings"
"sync"
"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"
@ -21,32 +19,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// bufferPool provides a sync.Pool of *bufio.Writers. This is used to reduce the
// amount of cgo calls, by writing bytes in larger chunks.
//
// Technically, httpcache already wraps its cached reader around a bufio.Reader,
// but we have no control over the buffer size.
var bufferPool = sync.Pool{
New: func() interface{} {
// Allocate a 512KB buffer by default.
const defaultBufSz = 512 * 1024
return bufio.NewWriterSize(nil, defaultBufSz)
},
}
func bufferedWriter(w io.Writer) *bufio.Writer {
buf := bufferPool.Get().(*bufio.Writer)
buf.Reset(w)
return buf
}
func returnBufferedWriter(buf *bufio.Writer) {
// Unreference the internal reader.
buf.Reset(nil)
bufferPool.Put(buf)
}
type ImageContainer interface { type ImageContainer interface {
primitives.Connector primitives.Connector
@ -93,9 +65,19 @@ func AsyncImage(ctx context.Context,
scale = surfaceContainer.GetScaleFactor() scale = surfaceContainer.GetScaleFactor()
} }
ctx = primitives.HandleDestroyCtx(ctx, img) ctx, cancel := context.WithCancel(ctx)
cancelHandle := img.Connect("destroy", func() {
log.Println("image destroyed, canceling")
cancel()
})
go func() { go func() {
// Ensure the contexts are cleaned up in the main thread.
defer gts.ExecAsync(func() {
img.HandlerDisconnect(cancelHandle)
cancel()
})
// Try and guess the MIME type from the URL. // Try and guess the MIME type from the URL.
mimeType := mime.TypeByExtension(urlExt(imageURL)) mimeType := mime.TypeByExtension(urlExt(imageURL))
@ -143,20 +125,11 @@ func AsyncImage(ctx context.Context,
l.Connect("area-prepared", load) l.Connect("area-prepared", load)
l.Connect("area-updated", load) l.Connect("area-updated", load)
// Borrow a buffered writer and return it at the end. if err := downloadImage(r.Body, l, procs, isGIF); err != nil {
bufWriter := bufferedWriter(l)
defer returnBufferedWriter(bufWriter)
if err := downloadImage(r.Body, bufWriter, procs, isGIF); err != nil {
log.Error(errors.Wrapf(err, "failed to download %q", imageURL)) log.Error(errors.Wrapf(err, "failed to download %q", imageURL))
// Force close after downloading. // Force close after downloading.
} }
if err := bufWriter.Flush(); err != nil {
log.Error(errors.Wrapf(err, "failed to flush writer for %q", imageURL))
// Force close after downloading.
}
if err := l.Close(); err != nil { if err := l.Close(); err != nil {
log.Error(errors.Wrapf(err, "failed to close pixbuf loader for %q", imageURL)) log.Error(errors.Wrapf(err, "failed to close pixbuf loader for %q", imageURL))
} }
@ -206,11 +179,13 @@ func loadFn(ctx context.Context, img ImageContainer, isGIF bool) func(l *gdk.Pix
} }
func execIfCtx(ctx context.Context, fn func()) { func execIfCtx(ctx context.Context, fn func()) {
gts.ExecLater(func() { if ctx.Err() == nil {
if ctx.Err() == nil { gts.ExecAsync(func() {
fn() if ctx.Err() == nil {
} fn()
}) }
})
}
} }
func downloadImage(src io.Reader, dst io.Writer, p []imgutil.Processor, isGIF bool) error { func downloadImage(src io.Reader, dst io.Writer, p []imgutil.Processor, isGIF bool) error {

View File

@ -17,16 +17,16 @@ var store = driver.NewStore(
) )
type Session struct { type Session struct {
ID string ID cchat.ID
// Metadata.
Name string Name string
Data map[string]string Data map[string]string
} }
// ConvertSession attempts to get the session data from the given cchat session. // ConvertSession attempts to get the session data from the given cchat session.
// It returns nil if it can't do it. // It returns nil if it can't do it.
func ConvertSession(ses cchat.Session) *Session { func ConvertSession(ses cchat.Session, name string) *Session {
var name = ses.Name().Content
saver := ses.AsSessionSaver() saver := ses.AsSessionSaver()
if saver == nil { if saver == nil {
return nil return nil
@ -45,28 +45,60 @@ func ConvertSession(ses cchat.Session) *Session {
} }
} }
func SaveSessions(service cchat.Service, sessions []Session) { // RestoreSession restores a single session.
if err := store.Set(service.Name().Content, sessions); err != nil { func RestoreSession(svc cchat.Service, sessionID cchat.ID) *Session {
log.Warn(errors.Wrap(err, "Error saving session")) service := Restore(svc)
} for _, session := range service.Sessions {
} if session.ID == sessionID {
// RestoreSessions restores all sessions of the service asynchronously, then
// calls the auth callback inside the Gtk main thread.
func RestoreSessions(service cchat.Service) (sessions []Session) {
// Ignore the error, it's not important.
if err := store.Get(service.Name().Content, &sessions); err != nil {
log.Warn(err)
}
return
}
func RestoreSession(service cchat.Service, id string) *Session {
var sessions = RestoreSessions(service)
for _, session := range sessions {
if session.ID == id {
return &session return &session
} }
} }
return nil return nil
} }
// Sessions is a list of sessions within a keyring. It provides an abstract way
// to save sessions with order.
type Service struct {
ID cchat.ID
Sessions []Session
}
// NewService creates a new service.
func NewService(svc cchat.Service, cap int) Service {
return Service{
ID: svc.ID(),
Sessions: make([]Session, 0, cap),
}
}
// Restore restores all sessions of the service asynchronously, then calls the
// auth callback inside the GTK main thread.
func Restore(svc cchat.Service) Service {
var sessions []Session
// Ignore the error, it's not important.
if err := store.Get(svc.ID(), &sessions); err != nil {
log.Warn(err)
}
return Service{
ID: svc.ID(),
Sessions: sessions,
}
}
// Add adds a session into the sessions list.
func (svc *Service) Add(ses cchat.Session, name string) {
s := ConvertSession(ses, name)
if s == nil {
return
}
svc.Sessions = append(svc.Sessions, *s)
}
// Save saves the sessions into the keyring.
func (svc Service) Save() {
if err := store.Set(svc.ID, svc.Sessions); err != nil {
log.Warn(errors.Wrap(err, "Error saving session"))
}
}

View File

@ -56,12 +56,12 @@ func SaveToFile(file string, v []byte) error {
f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_SYNC|os.O_TRUNC, 0644) f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_SYNC|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return errors.Wrap(err, "Failed to open file") return errors.Wrap(err, "failed to open file")
} }
defer f.Close() defer f.Close()
if _, err := f.Write(v); err != nil { if _, err := f.Write(v); err != nil {
return errors.Wrap(err, "Failed to write") return errors.Wrap(err, "failed to write")
} }
return nil return nil
@ -72,14 +72,19 @@ func SaveToFile(file string, v []byte) error {
func MarshalToFile(file string, from interface{}) error { func MarshalToFile(file string, from interface{}) error {
file = filepath.Join(DirPath(), file) file = filepath.Join(DirPath(), file)
dir := filepath.Dir(file)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return errors.Wrap(err, "failed to create config dir")
}
f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_SYNC|os.O_TRUNC, 0644) f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_SYNC|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return errors.Wrap(err, "Failed to open file") return errors.Wrap(err, "failed to open file")
} }
defer f.Close() defer f.Close()
if err := PrettyMarshal(f, from); err != nil { if err := PrettyMarshal(f, from); err != nil {
return errors.Wrap(err, "Failed to marshal given struct") return errors.Wrap(err, "failed to marshal given struct")
} }
return nil return nil

View File

@ -4,7 +4,7 @@ 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/ui/messages/container" "github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
) )
@ -12,40 +12,31 @@ type Container struct {
*container.ListContainer *container.ListContainer
} }
var _ container.Container = (*Container)(nil)
func NewContainer(ctrl container.Controller) *Container { func NewContainer(ctrl container.Controller) *Container {
c := container.NewListContainer(ctrl, constructors) c := container.NewListContainer(ctrl)
primitives.AddClass(c, "compact-container") primitives.AddClass(c, "compact-container")
return &Container{c} return &Container{c}
} }
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
msg := WrapPresendMessage(state)
c.AddMessage(msg)
return msg
}
func (c *Container) CreateMessage(msg cchat.MessageCreate) { func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() { gts.ExecAsync(func() {
c.ListContainer.CreateMessageUnsafe(msg) msg := WrapMessage(message.NewState(msg))
c.ListContainer.CleanMessages() c.ListContainer.AddMessage(msg)
}) })
} }
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) { func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
gts.ExecAsync(func() { c.ListContainer.UpdateMessageUnsafe(msg) }) gts.ExecAsync(func() { container.UpdateMessage(c, msg) })
} }
func (c *Container) DeleteMessage(msg cchat.MessageDelete) { func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
gts.ExecAsync(func() { c.ListContainer.DeleteMessageUnsafe(msg) }) gts.ExecAsync(func() { c.PopMessage(msg.ID()) })
}
var constructors = container.Constructor{
NewMessage: newMessage,
NewPresendMessage: newPresendMessage,
}
func newMessage(
msg cchat.MessageCreate, _ container.MessageRow) container.MessageRow {
return NewMessage(msg)
}
func newPresendMessage(
msg input.PresendMessage, _ container.MessageRow) container.PresendMessageRow {
return NewPresendMessage(msg)
} }

View File

@ -3,14 +3,13 @@ package compact
import ( import (
"time" "time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/humanize" "github.com/diamondburned/cchat-gtk/internal/humanize"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container" "github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri" "github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango" "github.com/gotk3/gotk3/pango"
) )
@ -29,41 +28,32 @@ var messageAuthorCSS = primitives.PrepareClassCSS("", `
`) `)
type PresendMessage struct { type PresendMessage struct {
message.PresendContainer message.Presender
Message Message
} }
func NewPresendMessage(msg input.PresendMessage) PresendMessage { func WrapPresendMessage(pstate *message.PresendState) PresendMessage {
msgc := message.NewPresendContainer(msg)
return PresendMessage{ return PresendMessage{
PresendContainer: msgc, Presender: pstate,
Message: wrapMessage(msgc.GenericContainer), Message: WrapMessage(pstate.State),
} }
} }
type Message struct { type Message struct {
*message.GenericContainer *message.State
Timestamp *gtk.Label Timestamp *gtk.Label
Username *labeluri.Label Username *labeluri.Label
unwrap func()
} }
var _ container.MessageRow = (*Message)(nil) var _ container.MessageRow = (*Message)(nil)
func NewMessage(msg cchat.MessageCreate) Message { func WrapMessage(ct *message.State) Message {
msgc := wrapMessage(message.NewContainer(msg))
message.FillContainer(msgc, msg)
return msgc
}
func NewEmptyMessage() Message {
ct := message.NewEmptyContainer()
return wrapMessage(ct)
}
func wrapMessage(ct *message.GenericContainer) Message {
ts := message.NewTimestamp() ts := message.NewTimestamp()
ts.SetVAlign(gtk.ALIGN_START) ts.SetVAlign(gtk.ALIGN_START)
ts.SetText(humanize.TimeAgo(ct.Time))
ts.SetTooltipText(ct.Time.Format(time.Stamp))
ts.Show() ts.Show()
messageTimeCSS(ts) messageTimeCSS(ts)
@ -80,31 +70,37 @@ func wrapMessage(ct *message.GenericContainer) Message {
ct.PackStart(ct.Content, true, true, 0) ct.PackStart(ct.Content, true, true, 0)
ct.SetClass("compact") ct.SetClass("compact")
rcfg := markup.RenderConfig{}
rcfg.NoReferencing = true
rcfg.SetForegroundAnchor(ct.ContentBodyStyle)
user.SetRenderer(func(rich text.Rich) markup.RenderOutput {
return markup.RenderCmplxWithConfig(rich, rcfg)
})
return Message{ return Message{
GenericContainer: ct, State: ct,
Timestamp: ts, Timestamp: ts,
Username: user, Username: user,
unwrap: ct.Author.Name.OnUpdate(func() {
user.SetLabel(ct.Author.Name.Label())
}),
} }
} }
// SetReferenceHighlighter sets the reference highlighter into the message. // SetReferenceHighlighter sets the reference highlighter into the message.
func (m Message) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) { func (m Message) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
m.GenericContainer.SetReferenceHighlighter(r) m.State.SetReferenceHighlighter(r)
m.Username.SetReferenceHighlighter(r) m.Username.SetReferenceHighlighter(r)
} }
func (m Message) UpdateTimestamp(t time.Time) { func (m Message) Unwrap(revert bool) *message.State {
m.GenericContainer.UpdateTimestamp(t) if revert {
m.Timestamp.SetText(humanize.TimeAgo(t)) m.unwrap()
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
} primitives.RemoveChildren(m)
m.SetClass("")
func (m Message) UpdateAuthor(author cchat.Author) { }
m.GenericContainer.UpdateAuthor(author)
return m.State
cfg := markup.RenderConfig{}
cfg.NoReferencing = true
cfg.SetForegroundAnchor(m.ContentBodyStyle)
m.Username.SetOutput(markup.RenderCmplxWithConfig(author.Name(), cfg))
} }

View File

@ -2,11 +2,8 @@ package container
import ( import (
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
"github.com/diamondburned/handy" "github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
@ -17,45 +14,43 @@ const BacklogLimit = 50
type MessageRow interface { type MessageRow interface {
message.Container message.Container
// Attach should only be called once. GrabFocus()
Row() *gtk.ListBoxRow
// AttachMenu should override the stored constructor.
AttachMenu(items []menu.Item) // save memory
// MenuItems returns the list of attached menu items.
MenuItems() []menu.Item
// SetReferenceHighlighter sets the reference highlighter into the message.
SetReferenceHighlighter(refer labeluri.ReferenceHighlighter)
} }
type PresendMessageRow interface { type PresendMessageRow interface {
MessageRow MessageRow
message.PresendContainer message.Presender
} }
// Container is a generic messages container for children messages for children // Container is a generic messages container for children messages for children
// packages. // packages.
type Container interface { type Container interface {
gtk.IWidget gtk.IWidget
cchat.MessagesContainer
// Reset resets the message container to its original state. // Reset resets the message container to its original state.
Reset() Reset()
// CreateMessageUnsafe creates a new message and returns the index that is // SetSelf sets the author for the current user.
// the location the message is added to. SetSelf(self *message.Author)
CreateMessageUnsafe(cchat.MessageCreate) MessageRow
UpdateMessageUnsafe(cchat.MessageUpdate) // NewPresendMessage creates and adds a presend message state into the list.
DeleteMessageUnsafe(cchat.MessageDelete) NewPresendMessage(state *message.PresendState) PresendMessageRow
// AddMessage adds a new message into the list.
AddMessage(row MessageRow)
// FirstMessage returns the first message in the buffer. Nil is returned if // FirstMessage returns the first message in the buffer. Nil is returned if
// there's nothing. // there's nothing.
FirstMessage() MessageRow FirstMessage() MessageRow
// AddPresendMessage adds and displays an unsent message. // LastMessage returns the last message in the buffer or nil if there's
AddPresendMessage(msg input.PresendMessage) PresendMessageRow // nothing.
// LatestMessageFrom returns the last message ID with that author. LastMessage() MessageRow
LatestMessageFrom(authorID string) (msgID string, ok bool) // Message finds and returns the message, if any. It performs maximum 2
// Message finds and returns the message, if any. // constant-time lookups.
Message(id cchat.ID, nonce string) MessageRow Message(id cchat.ID, nonce string) MessageRow
// FindMessage finds a message that satisfies the given callback. // FindMessage finds a message that satisfies the given callback. It
// iterates the message buffer from latest to earliest.
FindMessage(isMessage func(MessageRow) bool) MessageRow FindMessage(isMessage func(MessageRow) bool) MessageRow
// Highlight temporarily highlights the given message for a short while. // Highlight temporarily highlights the given message for a short while.
@ -67,6 +62,21 @@ type Container interface {
SetFocusVAdjustment(*gtk.Adjustment) SetFocusVAdjustment(*gtk.Adjustment)
} }
// UpdateMessage is a convenient function to update a message in the container.
// It does nothing if the message is not found.
func UpdateMessage(ct Container, update cchat.MessageUpdate) {
if msg := ct.Message(update.ID(), ""); msg != nil {
msg.UpdateContent(update.Content(), true)
}
}
// LatestMessageFrom returns the latest message from the given author ID.
func LatestMessageFrom(ct Container, authorID cchat.ID) MessageRow {
return ct.FindMessage(func(msg MessageRow) bool {
return msg.Unwrap(false).Author.ID == authorID
})
}
// Controller is for menu actions. // Controller is for menu actions.
type Controller interface { type Controller interface {
// Connector is used for button press events to unselect messages. // Connector is used for button press events to unselect messages.
@ -77,20 +87,13 @@ 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.Author) AuthorEvent(authorID cchat.ID)
// SelectMessage is called when a message is selected. // SelectMessage is called when a message is selected.
SelectMessage(list *ListStore, msg MessageRow) SelectMessage(list *ListStore, msg MessageRow)
// UnselectMessage is called when the message selection is cleared. // UnselectMessage is called when the message selection is cleared.
UnselectMessage() UnselectMessage()
} }
// Constructor is an interface for making custom message implementations which
// allows ListContainer to generically work with.
type Constructor struct {
NewMessage func(msg cchat.MessageCreate, before MessageRow) MessageRow
NewPresendMessage func(msg input.PresendMessage, before MessageRow) PresendMessageRow
}
const ColumnSpacing = 8 const ColumnSpacing = 8
// ListContainer is an implementation of Container, which allows flexible // ListContainer is an implementation of Container, which allows flexible
@ -106,7 +109,7 @@ type ListContainer struct {
// messageRow w/ required internals // messageRow w/ required internals
type messageRow struct { type messageRow struct {
MessageRow MessageRow
presend message.PresendContainer // this shouldn't be here but i'm lazy presend message.Presender // this shouldn't be here but i'm lazy
} }
// unwrapRow is a helper that unwraps a messageRow if it's not nil. If it's nil, // unwrapRow is a helper that unwraps a messageRow if it's not nil. If it's nil,
@ -118,10 +121,8 @@ func unwrapRow(msg *messageRow) MessageRow {
return msg.MessageRow return msg.MessageRow
} }
var _ Container = (*ListContainer)(nil) func NewListContainer(ctrl Controller) *ListContainer {
listStore := NewListStore(ctrl)
func NewListContainer(ctrl Controller, constr Constructor) *ListContainer {
listStore := NewListStore(ctrl, constr)
listStore.ListBox.Show() listStore.ListBox.Show()
clamp := handy.ClampNew() clamp := handy.ClampNew()
@ -139,12 +140,10 @@ func NewListContainer(ctrl Controller, constr Constructor) *ListContainer {
} }
} }
// TODO: remove useless abstraction (this file). func (c *ListContainer) AddMessage(row MessageRow) {
c.ListStore.AddMessage(row)
// // CreateMessageUnsafe inserts a message. It does not clean up old messages. c.CleanMessages()
// func (c *ListContainer) CreateMessageUnsafe(msg cchat.MessageCreate) MessageRow { }
// return c.ListStore.CreateMessageUnsafe(msg)
// }
// CleanMessages cleans up the oldest messages if the user is scrolled to the // CleanMessages cleans up the oldest messages if the user is scrolled to the
// bottom. True is returned if there were changes. // bottom. True is returned if there were changes.

View File

@ -0,0 +1,47 @@
package cozy
import (
"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/gotk3/gotk3/gtk"
)
type Avatar struct {
roundimage.Button
Image *roundimage.StillImage
url string
}
func NewAvatar(parent primitives.Connector) *Avatar {
img := roundimage.NewStillImage(nil, 0)
img.SetSizeRequest(AvatarSize, AvatarSize)
img.Show()
avatar, _ := roundimage.NewCustomButton(img)
avatar.SetVAlign(gtk.ALIGN_START)
// Default icon.
primitives.SetImageIcon(img, "user-available-symbolic", AvatarSize)
return &Avatar{*avatar, img, ""}
}
// SetImage sets the avatar from the given label image.
func (a *Avatar) SetImage(img rich.LabelImage) {
a.SetURL(img.URL)
}
// SetURL updates the Avatar to be that URL. It does nothing if URL is empty or
// matches the existing one.
func (a *Avatar) SetURL(url string) {
// Check if the URL is the same. This will save us quite a few requests, as
// some methods rely on the side-effects of other methods, and they may call
// UpdateAuthor multiple times.
if a.url == url || url == "" {
return
}
a.url = url
a.Image.SetImageURL(url)
}

View File

@ -6,24 +6,10 @@ 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/ui/messages/container" "github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
) )
// Unwrapper provides an interface for messages to be unwrapped. This is used to
// convert between collapsed and full messages.
type Unwrapper interface {
Unwrap() *message.GenericContainer
}
var (
_ Unwrapper = (*CollapsedMessage)(nil)
_ Unwrapper = (*CollapsedSendingMessage)(nil)
_ Unwrapper = (*FullMessage)(nil)
_ Unwrapper = (*FullSendingMessage)(nil)
)
// Collapsible is an interface for cozy messages to return whether or not // Collapsible is an interface for cozy messages to return whether or not
// they're full or collapsed. // they're full or collapsed.
type Collapsible interface { type Collapsible interface {
@ -43,29 +29,26 @@ const (
AvatarMargin = 10 AvatarMargin = 10
) )
var messageConstructors = container.Constructor{ // NewMessage creates a new message.
NewMessage: NewMessage,
NewPresendMessage: NewPresendMessage,
}
func NewMessage( func NewMessage(
msg cchat.MessageCreate, before container.MessageRow) container.MessageRow { s *message.State, before container.MessageRow) container.MessageRow {
if isCollapsible(before, msg) { if isCollapsible(before, s) {
return NewCollapsedMessage(msg) return WrapCollapsedMessage(s)
} }
return NewFullMessage(msg) return WrapFullMessage(s)
} }
// NewPresendMessage creates a new presend message.
func NewPresendMessage( func NewPresendMessage(
msg input.PresendMessage, before container.MessageRow) container.PresendMessageRow { s *message.PresendState, before container.MessageRow) container.PresendMessageRow {
if isCollapsible(before, msg) { if isCollapsible(before, s.State) {
return NewCollapsedSendingMessage(msg) return WrapCollapsedSendingMessage(s)
} }
return NewFullSendingMessage(msg) return WrapFullSendingMessage(s)
} }
type Container struct { type Container struct {
@ -73,7 +56,7 @@ type Container struct {
} }
func NewContainer(ctrl container.Controller) *Container { func NewContainer(ctrl container.Controller) *Container {
c := container.NewListContainer(ctrl, messageConstructors) c := container.NewListContainer(ctrl)
primitives.AddClass(c, "cozy-container") primitives.AddClass(c, "cozy-container")
return &Container{ListContainer: c} return &Container{ListContainer: c}
} }
@ -81,105 +64,72 @@ func NewContainer(ctrl container.Controller) *Container {
func (c *Container) findAuthorID(authorID string) container.MessageRow { func (c *Container) findAuthorID(authorID string) container.MessageRow {
// Search the old author if we have any. // Search the old author if we have any.
return c.ListStore.FindMessage(func(msgc container.MessageRow) bool { return c.ListStore.FindMessage(func(msgc container.MessageRow) bool {
return msgc.Author().ID() == authorID return msgc.Unwrap(false).Author.ID == authorID
}) })
} }
// reuseAvatar tries to search past messages with the same author ID and URL for
// the image. It will fetch anew if there's none.
func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) {
// Is this a message that we can work with? We have to assert to
// FullSendingMessage because that's where our messages are.
var lastAuthorMsg = c.findAuthorID(authorID)
// Borrow the avatar pixbuf, but only if the avatar URL is the same.
p, ok := lastAuthorMsg.(AvatarPixbufCopier)
if ok && lastAuthorMsg.Author().Avatar() == avatarURL {
if p.CopyAvatarPixbuf(full.Avatar.Image) {
full.Avatar.ManuallySetURL(avatarURL)
return
}
}
// We can't borrow, so we need to fetch it anew.
full.Avatar.SetURL(avatarURL)
}
// lastMessageIsAuthor removed - assuming index before insertion is harmful.
type authoredMessage interface {
cchat.MessageHeader
Author() cchat.Author
}
var (
_ authoredMessage = (cchat.MessageCreate)(nil)
_ authoredMessage = (input.PresendMessage)(nil)
_ authoredMessage = (container.MessageRow)(nil)
_ authoredMessage = (container.PresendMessageRow)(nil)
)
const splitDuration = 10 * time.Minute const splitDuration = 10 * time.Minute
// isCollapsible returns true if the given lastMsg has matching conditions with // isCollapsible returns true if the given lastMsg has matching conditions with
// the given msg. // the given msg.
func isCollapsible(lastMsg container.MessageRow, msg authoredMessage) bool { func isCollapsible(last container.MessageRow, msg *message.State) bool {
if lastMsg == nil || msg == nil { if last == nil || msg.ID == "" {
return false return false
} }
lastAuthor := lastMsg.Author() lastMsg := last.Unwrap(false)
thisAuthor := msg.Author()
return true && return true &&
lastAuthor.ID() == thisAuthor.ID() && lastMsg.Author.ID == msg.ID &&
lastAuthor.Name().String() == thisAuthor.Name().String() && lastMsg.Time.Add(splitDuration).After(msg.Time)
lastMsg.Time().Add(splitDuration).After(msg.Time()) }
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
msgr := NewPresendMessage(state, c.LastMessage())
c.AddMessage(msgr)
return msgr
} }
func (c *Container) CreateMessage(msg cchat.MessageCreate) { func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() { gts.ExecAsync(func() {
// Create the message in the parent's handler. This handler will also state := message.NewState(msg)
// wipe old messages. msgr := NewMessage(state, c.LastMessage())
row := c.ListContainer.CreateMessageUnsafe(msg)
// Is this a full message? If so, then we should fetch the avatar when c.AddMessage(msgr)
// we can.
if full, ok := row.(*FullMessage); ok {
author := msg.Author()
avatarURL := author.Avatar()
// Try and reuse an existing avatar if the author has one.
if avatarURL != "" {
// Try reusing the avatar, but fetch it from the internet if we can't
// reuse. The reuse function does this for us.
c.reuseAvatar(author.ID(), avatarURL, full)
}
}
// Did the handler wipe old messages? It will only do so if the user is
// scrolled to the bottom.
if c.ListContainer.CleanMessages() {
// We need to uncollapse the first (top) message. No length check is
// needed here, as we just inserted a message.
c.uncompact(c.FirstMessage())
}
// If we've prepended the message, then see if we need to collapse the
// second message.
if first := c.ListContainer.FirstMessage(); first != nil && first.ID() == msg.ID() {
// If the author is the same, then collapse.
if sec := c.NthMessage(1); sec != nil && isCollapsible(sec, msg) {
c.compact(sec)
}
}
}) })
} }
// AddMessage adds the given message.
func (c *Container) AddMessage(msgr container.MessageRow) {
// Create the message in the parent's handler. This handler will also
// wipe old messages.
c.ListContainer.AddMessage(msgr)
// Did the handler wipe old messages? It will only do so if the user is
// scrolled to the bottom.
if c.ListContainer.CleanMessages() {
// We need to uncollapse the first (top) message. No length check is
// needed here, as we just inserted a message.
c.uncompact(c.FirstMessage())
}
// If we've prepended the message, then see if we need to collapse the
// second message.
if first := c.ListContainer.FirstMessage(); first != nil {
firstState := first.Unwrap(false)
msgState := msgr.Unwrap(false)
if firstState.ID == msgState.ID {
// If the author is the same, then collapse.
if sec := c.NthMessage(1); isCollapsible(sec, firstState) {
c.compact(sec)
}
}
}
}
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) { func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
gts.ExecAsync(func() { gts.ExecAsync(func() { container.UpdateMessage(c, msg) })
c.UpdateMessageUnsafe(msg)
})
} }
func (c *Container) DeleteMessage(msg cchat.MessageDelete) { func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
@ -202,10 +152,13 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
return return
} }
msgAuthorID := msg.Author().ID() msgHeader := msg.Unwrap(false)
prevHeader := prev.Unwrap(false)
nextHeader := next.Unwrap(false)
// Check if the last message is the author's (relative to i): // Check if the last message is the author's (relative to i):
if prev.Author().ID() == msgAuthorID { if prevHeader.Author.ID == msgHeader.Author.ID {
// If the author is the same, then we don't need to uncollapse the // If the author is the same, then we don't need to uncollapse the
// message. // message.
return return
@ -213,7 +166,7 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
// If the next message (relative to i) is not the deleted message's // If the next message (relative to i) is not the deleted message's
// author, then we don't need to uncollapse it. // author, then we don't need to uncollapse it.
if next.Author().ID() != msgAuthorID { if nextHeader.Author.ID != msgHeader.Author.ID {
return return
} }
@ -223,33 +176,11 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
} }
func (c *Container) uncompact(msg container.MessageRow) { func (c *Container) uncompact(msg container.MessageRow) {
// We should only uncompact the message if it's compacted in the first full := WrapFullMessage(msg.Unwrap(true))
// place.
compact, ok := msg.(*CollapsedMessage)
if !ok {
return
}
// Start the "lengthy" uncollapse process.
full := WrapFullMessage(compact.Unwrap())
// Update the container to reformat everything including the timestamps.
message.RefreshContainer(full, full.GenericContainer)
// Update the avatar if needed be, since we're now showing it.
author := msg.Author()
c.reuseAvatar(author.ID(), author.Avatar(), full)
// Swap the old next message out for a new one.
c.ListStore.SwapMessage(full) c.ListStore.SwapMessage(full)
} }
func (c *Container) compact(msg container.MessageRow) { func (c *Container) compact(msg container.MessageRow) {
full, ok := msg.(*FullMessage) compact := WrapCollapsedMessage(msg.Unwrap(true))
if !ok {
return
}
compact := WrapCollapsedMessage(full.Unwrap())
message.RefreshContainer(compact, compact.GenericContainer)
c.ListStore.SwapMessage(compact) c.ListStore.SwapMessage(compact)
} }

View File

@ -1,12 +1,7 @@
package cozy package cozy
import ( import (
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/humanize"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container" "github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
@ -15,17 +10,12 @@ import (
// the header, and the avatar is invisible. // the header, and the avatar is invisible.
type CollapsedMessage struct { type CollapsedMessage struct {
// Author is still updated normally. // Author is still updated normally.
*message.GenericContainer *message.State
Timestamp *gtk.Label Timestamp *gtk.Label
} }
func NewCollapsedMessage(msg cchat.MessageCreate) *CollapsedMessage { // WrapCollapsedMessage wraps the given message state to be a collapsed message.
msgc := WrapCollapsedMessage(message.NewContainer(msg)) func WrapCollapsedMessage(gc *message.State) *CollapsedMessage {
message.FillContainer(msgc, msg)
return msgc
}
func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
// Set Timestamp's padding accordingly to Avatar's. // Set Timestamp's padding accordingly to Avatar's.
ts := message.NewTimestamp() ts := message.NewTimestamp()
ts.SetSizeRequest(AvatarSize, -1) ts.SetSizeRequest(AvatarSize, -1)
@ -42,37 +32,31 @@ func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
gc.SetClass("cozy-collapsed") gc.SetClass("cozy-collapsed")
return &CollapsedMessage{ return &CollapsedMessage{
GenericContainer: gc, State: gc,
Timestamp: ts, Timestamp: ts,
} }
} }
func (c *CollapsedMessage) Collapsed() bool { return true } func (c *CollapsedMessage) Collapsed() bool { return true }
func (c *CollapsedMessage) UpdateTimestamp(t time.Time) { func (c *CollapsedMessage) Unwrap(revert bool) *message.State {
c.GenericContainer.UpdateTimestamp(t) if revert {
c.Timestamp.SetText(humanize.TimeAgoShort(t)) // Remove State's widgets from the containers.
} c.Remove(c.Timestamp)
c.Remove(c.Content)
}
func (c *CollapsedMessage) Unwrap() *message.GenericContainer { return c.State
// Remove GenericContainer's widgets from the containers.
c.Remove(c.Timestamp)
c.Remove(c.Content)
// Return after removing.
return c.GenericContainer
} }
type CollapsedSendingMessage struct { type CollapsedSendingMessage struct {
*CollapsedMessage *CollapsedMessage
message.PresendContainer message.Presender
} }
func NewCollapsedSendingMessage(msg input.PresendMessage) *CollapsedSendingMessage { func WrapCollapsedSendingMessage(pstate *message.PresendState) *CollapsedSendingMessage {
var msgc = message.NewPresendContainer(msg)
return &CollapsedSendingMessage{ return &CollapsedSendingMessage{
CollapsedMessage: WrapCollapsedMessage(msgc.GenericContainer), CollapsedMessage: WrapCollapsedMessage(pstate.State),
PresendContainer: msgc, Presender: pstate,
} }
} }

View File

@ -4,18 +4,13 @@ import (
"time" "time"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/humanize" "github.com/diamondburned/cchat-gtk/internal/humanize"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container" "github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"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/rich/labeluri" "github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/cairo"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
@ -23,22 +18,20 @@ import (
const TopFullMargin = 4 const TopFullMargin = 4
type FullMessage struct { type FullMessage struct {
*message.GenericContainer *message.State
// Grid widgets. // Grid widgets.
Avatar *Avatar Avatar *Avatar
MainBox *gtk.Box // wraps header and content MainBox *gtk.Box // wraps header and content
Header *labeluri.Label HeaderLabel *labeluri.Label
timestamp string // markup timestamp string // markup
}
type AvatarPixbufCopier interface { // unwrap is used to removing label handlers.
CopyAvatarPixbuf(img httputil.SurfaceContainer) bool unwrap func()
} }
var ( var (
_ AvatarPixbufCopier = (*FullMessage)(nil)
_ message.Container = (*FullMessage)(nil) _ message.Container = (*FullMessage)(nil)
_ container.MessageRow = (*FullMessage)(nil) _ container.MessageRow = (*FullMessage)(nil)
) )
@ -51,22 +44,16 @@ var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", `
`) `)
func NewFullMessage(msg cchat.MessageCreate) *FullMessage { func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
msgc := WrapFullMessage(message.NewContainer(msg)) return WrapFullMessage(message.NewState(msg))
// Don't update the avatar. NewMessage in controller will try and reuse the
// pixbuf if possible.
msgc.UpdateAuthorName(msg.Author().Name())
msgc.UpdateTimestamp(msg.Time())
msgc.UpdateContent(msg.Content(), false)
return msgc
} }
func WrapFullMessage(gc *message.GenericContainer) *FullMessage { func WrapFullMessage(gc *message.State) *FullMessage {
header := labeluri.NewLabel(text.Rich{}) header := labeluri.NewLabel(text.Rich{})
header.SetHAlign(gtk.ALIGN_START) // left-align header.SetHAlign(gtk.ALIGN_START) // left-align
header.SetMaxWidthChars(100) header.SetMaxWidthChars(100)
header.Show() header.Show()
avatar := NewAvatar() avatar := NewAvatar(gc.Row)
avatar.SetMarginTop(TopFullMargin / 2) avatar.SetMarginTop(TopFullMargin / 2)
avatar.SetMarginStart(container.ColumnSpacing * 2) avatar.SetMarginStart(container.ColumnSpacing * 2)
avatar.Connect("clicked", func(w gtk.IWidget) { avatar.Connect("clicked", func(w gtk.IWidget) {
@ -97,87 +84,61 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
gc.PackStart(main, true, true, 0) gc.PackStart(main, true, true, 0)
gc.SetClass("cozy-full") gc.SetClass("cozy-full")
return &FullMessage{ msg := &FullMessage{
GenericContainer: gc, State: gc,
timestamp: formatLongTime(gc.Time),
Avatar: avatar, Avatar: avatar,
MainBox: main, MainBox: main,
Header: header, HeaderLabel: header,
unwrap: gc.Author.Name.OnUpdate(func() {
avatar.SetImage(gc.Author.Name.Image())
header.SetLabel(gc.Author.Name.Label())
}),
} }
header.SetRenderer(func(rich text.Rich) markup.RenderOutput {
cfg := markup.RenderConfig{}
cfg.NoReferencing = true
cfg.SetForegroundAnchor(gc.ContentBodyStyle)
output := markup.RenderCmplxWithConfig(rich, cfg)
output.Markup = `<span font_weight="600">` + output.Markup + "</span>"
output.Markup += msg.timestamp
return output
})
return msg
} }
func (m *FullMessage) Collapsed() bool { return false } func (m *FullMessage) Collapsed() bool { return false }
func (m *FullMessage) Unwrap() *message.GenericContainer { func (m *FullMessage) Unwrap(revert bool) *message.State {
// Remove GenericContainer's widgets from the containers. if revert {
m.Header.Destroy() // Remove the handlers.
m.MainBox.Remove(m.Content) // not ours, so don't destroy. m.unwrap()
// Remove the message from the grid. // Remove State's widgets from the containers.
m.Avatar.Destroy() m.HeaderLabel.Destroy()
m.MainBox.Destroy() m.MainBox.Remove(m.Content) // not ours, so don't destroy.
// Return after removing. // Remove the message from the grid.
return m.GenericContainer m.Avatar.Destroy()
} m.MainBox.Destroy()
func (m *FullMessage) UpdateTimestamp(t time.Time) {
m.GenericContainer.UpdateTimestamp(t)
m.timestamp = " " +
`<span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>`
// Update the timestamp.
m.Header.SetMarkup(m.Header.Output().Markup + m.timestamp)
}
func (m *FullMessage) UpdateAuthor(author cchat.Author) {
// Call the parent's method to update the state.
m.GenericContainer.UpdateAuthor(author)
m.UpdateAuthorName(author.Name())
m.Avatar.SetURL(author.Avatar())
}
func (m *FullMessage) UpdateAuthorName(name text.Rich) {
cfg := markup.RenderConfig{}
cfg.NoReferencing = true
cfg.SetForegroundAnchor(m.ContentBodyStyle)
output := markup.RenderCmplxWithConfig(name, cfg)
output.Markup = `<span font_weight="600">` + output.Markup + "</span>"
m.Header.SetMarkup(output.Markup + m.timestamp)
m.Header.SetUnderlyingOutput(output)
}
// CopyAvatarPixbuf sets the pixbuf into the given container. This shares the
// same pixbuf, but gtk.Image should take its own reference from the pixbuf.
func (m *FullMessage) CopyAvatarPixbuf(dst httputil.SurfaceContainer) bool {
switch img := m.Avatar.Image.GetImage(); img.GetStorageType() {
case gtk.IMAGE_PIXBUF:
dst.SetFromPixbuf(img.GetPixbuf())
case gtk.IMAGE_ANIMATION:
dst.SetFromAnimation(img.GetAnimation())
case gtk.IMAGE_SURFACE:
v, _ := img.GetProperty("surface")
dst.SetFromSurface(v.(*cairo.Surface))
default:
return false
} }
return true
return m.State
} }
func (m *FullMessage) AttachMenu(items []menu.Item) { func formatLongTime(t time.Time) string {
// Bind to parent's container as well. return `<span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>`
m.GenericContainer.AttachMenu(items)
// Bind to the box.
// TODO lol
} }
type FullSendingMessage struct { type FullSendingMessage struct {
message.PresendContainer *FullMessage
FullMessage message.Presender
} }
var ( var (
@ -185,51 +146,9 @@ var (
_ container.MessageRow = (*FullSendingMessage)(nil) _ container.MessageRow = (*FullSendingMessage)(nil)
) )
func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage { func WrapFullSendingMessage(pstate *message.PresendState) *FullSendingMessage {
var msgc = message.NewPresendContainer(msg)
return &FullSendingMessage{ return &FullSendingMessage{
PresendContainer: msgc, FullMessage: WrapFullMessage(pstate.State),
FullMessage: *WrapFullMessage(msgc.GenericContainer), Presender: pstate,
} }
} }
type Avatar struct {
roundimage.Button
Image *roundimage.StaticImage
url string
}
func NewAvatar() *Avatar {
img, _ := roundimage.NewStaticImage(nil, 0)
img.SetSizeRequest(AvatarSize, AvatarSize)
img.Show()
avatar, _ := roundimage.NewCustomButton(img)
avatar.SetVAlign(gtk.ALIGN_START)
// Default icon.
primitives.SetImageIcon(img, "user-available-symbolic", AvatarSize)
return &Avatar{*avatar, img, ""}
}
// SetURL updates the Avatar to be that URL. It does nothing if URL is empty or
// matches the existing one.
func (a *Avatar) SetURL(url string) {
// Check if the URL is the same. This will save us quite a few requests, as
// some methods rely on the side-effects of other methods, and they may call
// UpdateAuthor multiple times.
if a.url == url || url == "" {
return
}
a.url = url
a.Image.SetImageURL(url)
}
// ManuallySetURL sets the URL without downloading the image. It assumes the
// pixbuf is borrowed elsewhere.
func (a *Avatar) ManuallySetURL(url string) {
a.url = url
}

View File

@ -1,15 +1,15 @@
package container package container
import ( import (
"log"
"strings" "strings"
"time" "time"
"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/ui/messages/input" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
@ -63,18 +63,19 @@ var messageListCSS = primitives.PrepareClassCSS("message-list", `
.message-list { background: transparent; } .message-list { background: transparent; }
`) `)
type ListStore struct { var fallbackAuthor = message.NewCustomAuthor("", text.Plain("self"))
ListBox *gtk.ListBox
Construct Constructor type ListStore struct {
ListBox *gtk.ListBox
Controller Controller Controller Controller
resetMe bool self *message.Author
resetMe bool
messages map[messageKey]*messageRow messages map[messageKey]*messageRow
} }
func NewListStore(ctrl Controller, constr Constructor) *ListStore { func NewListStore(ctrl Controller) *ListStore {
listBox, _ := gtk.ListBoxNew() listBox, _ := gtk.ListBoxNew()
listBox.SetSelectionMode(gtk.SELECTION_SINGLE) listBox.SetSelectionMode(gtk.SELECTION_SINGLE)
listBox.Show() listBox.Show()
@ -82,9 +83,9 @@ func NewListStore(ctrl Controller, constr Constructor) *ListStore {
listStore := ListStore{ listStore := ListStore{
ListBox: listBox, ListBox: listBox,
Construct: constr,
Controller: ctrl, Controller: ctrl,
messages: make(map[messageKey]*messageRow, BacklogLimit+1), messages: make(map[messageKey]*messageRow, BacklogLimit+1),
self: &fallbackAuthor,
} }
var selected bool var selected bool
@ -113,9 +114,29 @@ func NewListStore(ctrl Controller, constr Constructor) *ListStore {
return &listStore return &listStore
} }
// Reset resets the list store.
func (c *ListStore) Reset() { func (c *ListStore) Reset() {
for _, msg := range c.messages {
destroyMsg(msg)
}
// Delegate removing children to the constructor. // Delegate removing children to the constructor.
c.messages = make(map[messageKey]*messageRow, BacklogLimit+1) c.messages = make(map[messageKey]*messageRow, BacklogLimit+1)
if c.self.ID != "" {
c.self.Name.Stop()
}
}
// SetSelf sets the current author to presend. If ID is empty or Namer is nil,
// then the fallback author is used instead. The given author will be stopped
// on reset.
func (c *ListStore) SetSelf(self *message.Author) {
if self != nil {
c.self = self
} else {
c.self = &fallbackAuthor
}
} }
func (c *ListStore) MessagesLen() int { func (c *ListStore) MessagesLen() int {
@ -133,22 +154,27 @@ func (c *ListStore) SwapMessage(msg MessageRow) bool {
msg = m.MessageRow msg = m.MessageRow
} }
msgState := msg.Unwrap(false)
// Get the current message's index. // Get the current message's index.
oldMsg, ix := c.findIndex(msg.ID()) oldMsg, ix := c.findIndex(msgState.ID)
if ix == -1 { if ix == -1 {
return false return false
} }
oldState := oldMsg.Unwrap(false)
// Remove the to-be-replaced message box. We should probably reuse the row. // Remove the to-be-replaced message box. We should probably reuse the row.
c.ListBox.Remove(oldMsg.Row()) c.ListBox.Remove(oldState.Row)
// Add a row at index. The actual row we want to delete will be shifted // Add a row at index. The actual row we want to delete will be shifted
// downwards. // downwards.
c.ListBox.Insert(msg.Row(), ix) c.ListBox.Insert(msgState.Row, ix)
// Set the message into the map. // Set the message into the map.
row := c.messages[idKey(msg.ID())] row := c.messages[idKey(msgState.ID)]
row.MessageRow = msg row.MessageRow = msg
c.bindMessage(row)
return true return true
} }
@ -168,8 +194,6 @@ func (c *ListStore) Around(id cchat.ID) (before, after MessageRow) {
} }
func (c *ListStore) around(aroundID cchat.ID) (before, after *messageRow) { func (c *ListStore) around(aroundID cchat.ID) (before, after *messageRow) {
c.ensureEmpty()
var last *messageRow var last *messageRow
var next bool var next bool
@ -192,25 +216,8 @@ func (c *ListStore) around(aroundID cchat.ID) (before, after *messageRow) {
return return
} }
// LatestMessageFrom returns the latest message with the given user ID. This is
// used for the input prompt.
func (c *ListStore) LatestMessageFrom(userID string) (msgID string, ok bool) {
// FindMessage already looks from the latest messages.
var msg = c.FindMessage(func(msg MessageRow) bool {
return msg.Author().ID() == userID
})
if msg == nil {
return "", false
}
return msg.ID(), true
}
// findIndex searches backwards for id. // findIndex searches backwards for id.
func (c *ListStore) findIndex(findID cchat.ID) (found *messageRow, index int) { func (c *ListStore) findIndex(findID cchat.ID) (found *messageRow, index int) {
c.ensureEmpty()
// Faster implementation of findMessage: no map lookup is done until an ID // Faster implementation of findMessage: no map lookup is done until an ID
// match, so the worst case is a single string hash. // match, so the worst case is a single string hash.
index = c.MessagesLen() - 1 index = c.MessagesLen() - 1
@ -235,8 +242,6 @@ func (c *ListStore) findIndex(findID cchat.ID) (found *messageRow, index int) {
} }
func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int) { func (c *ListStore) findMessage(presend bool, fn func(*messageRow) bool) (*messageRow, int) {
c.ensureEmpty()
var r *messageRow var r *messageRow
var i = c.MessagesLen() - 1 var i = c.MessagesLen() - 1
@ -316,6 +321,9 @@ func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
if nonce != "" { if 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[nonceKey(nonce)] m, ok := c.messages[nonceKey(nonce)]
// This is honestly pretty dumb, but whatever.
// TODO: make message() getter not set.
if ok { if ok {
// Replace the nonce key with ID. // Replace the nonce key with ID.
delete(c.messages, nonceKey(nonce)) delete(c.messages, nonceKey(nonce))
@ -334,135 +342,69 @@ func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
return nil return nil
} }
// ensureEmpty ensures that if the message map is empty, then the container
// should also be.
func (c *ListStore) ensureEmpty() {
if len(c.messages) == 0 {
primitives.RemoveChildren(c.ListBox)
}
}
func (c *ListStore) bindMessage(msgc *messageRow) { func (c *ListStore) bindMessage(msgc *messageRow) {
state := msgc.Unwrap(false)
// Bind the message ID to the row so we can easily do a lookup. // Bind the message ID to the row so we can easily do a lookup.
var key messageKey key := messageKey{
if id := msgc.ID(); id != "" { id: state.ID,
key.id = id }
} else {
key.id = msgc.Nonce() if state.Nonce != "" {
key.id = state.Nonce
key.nonce = true key.nonce = true
} }
msgc.Row().SetName(key.name()) state.Row.SetName(key.name())
msgc.SetReferenceHighlighter(c) msgc.MessageRow.SetReferenceHighlighter(c)
c.Controller.BindMenu(msgc.MessageRow) c.Controller.BindMenu(msgc.MessageRow)
} }
// AddPresendMessage inserts an input.PresendMessage into the container and func (c *ListStore) AddMessage(msg MessageRow) {
// returning a wrapped widget interface. state := msg.Unwrap(false)
func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRow {
c.ensureEmpty()
before := c.LastMessage() defer c.Controller.AuthorEvent(state.Author.ID)
if before != nil {
log.Println("Found before:", before.Author().Name())
} else {
log.Println("Before is nil")
}
presend := c.Construct.NewPresendMessage(msg, before)
msgc := &messageRow{
MessageRow: presend,
presend: presend,
}
// Set the message into the list.
c.ListBox.Insert(msgc.Row(), c.MessagesLen())
// Set the NONCE into the message map.
c.messages[nonceKey(msgc.Nonce())] = msgc
c.bindMessage(msgc)
return presend
}
// Many attempts were made to have CreateMessageUnsafe return an index. That is
// unreliable. The index might be off if the message buffer is cleaned up. Don't
// rely on it.
func (c *ListStore) CreateMessageUnsafe(msg cchat.MessageCreate) MessageRow {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
// Do not attempt to update before insertion (aka upsert). // Do not attempt to update before insertion (aka upsert).
if msgc := c.message(msg.ID(), msg.Nonce()); msgc != nil { if msgc := c.message(state.ID, state.Nonce); msgc != nil {
msgc.UpdateAuthor(msg.Author()) // This is kind of expensive, but it shouldn't really matter.
msgc.UpdateContent(msg.Content(), false) c.SwapMessage(msg)
msgc.UpdateTimestamp(msg.Time()) return
c.bindMessage(msgc)
return msgc.MessageRow
} }
c.ensureEmpty()
msgTime := msg.Time()
// Iterate and compare timestamp to find where to insert a message. Note // Iterate and compare timestamp to find where to insert a message. Note
// that "before" is the message that will go before the to-be-inserted // that "before" is the message that will go before the to-be-inserted
// method. // method.
before, index := c.findMessage(true, func(before *messageRow) bool { before, index := c.findMessage(true, func(before *messageRow) bool {
return msgTime.After(before.Time()) return before.Unwrap(false).Time.After(state.Time)
}) })
msgc := &messageRow{ msgc := &messageRow{
MessageRow: c.Construct.NewMessage(msg, unwrapRow(before)), MessageRow: msg,
} }
// Add the message. If before is nil, then the to-be-inserted message is the // Add the message. If before is nil, then the to-be-inserted message is the
// earliest message, therefore we prepend it. // earliest message, therefore we prepend it.
if before == nil { if before == nil {
index = 0 index = 0
c.ListBox.Prepend(msgc.Row()) c.ListBox.Prepend(state.Row)
} else { } else {
index++ // insert right after index++ // insert right after
// Fast path: Insert did appear a lot on profiles, so we can try and use // Fast path: Insert did appear a lot on profiles, so we can try and use
// Add over Insert when we know. // Add over Insert when we know.
if c.MessagesLen() == index { if c.MessagesLen() == index {
c.ListBox.Add(msgc.Row()) c.ListBox.Add(state.Row)
} else { } else {
c.ListBox.Insert(msgc.Row(), index) c.ListBox.Insert(state.Row, index)
} }
} }
// Set the ID into the message map. // Set the ID into the message map.
c.messages[idKey(msgc.ID())] = msgc c.messages[idKey(state.ID)] = msgc
c.bindMessage(msgc) c.bindMessage(msgc)
return msgc.MessageRow
}
func (c *ListStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
if msgc := c.Message(msg.ID(), ""); msgc != nil {
if author := msg.Author(); author != nil {
msgc.UpdateAuthor(author)
}
if content := msg.Content(); !content.IsEmpty() {
msgc.UpdateContent(content, true)
}
}
return
}
func (c *ListStore) DeleteMessageUnsafe(msg cchat.MessageDelete) {
c.PopMessage(msg.ID())
} }
// PopMessage deletes a message off of the list and return the deleted message. // PopMessage deletes a message off of the list and return the deleted message.
@ -475,7 +417,8 @@ func (c *ListStore) PopMessage(id cchat.ID) (msg MessageRow) {
msg = gridMsg.MessageRow msg = gridMsg.MessageRow
// Remove off of the Gtk grid. // Remove off of the Gtk grid.
gridMsg.Row().Destroy() destroyMsg(gridMsg)
// Delete off the map. // Delete off the map.
delete(c.messages, idKey(id)) delete(c.messages, idKey(id))
@ -489,22 +432,23 @@ func (c *ListStore) DeleteEarliest(n int) {
return return
} }
c.ensureEmpty()
// Since container/list nils out the next element, we can't just call Next // Since container/list nils out the next element, we can't just call Next
// after deleting, so we have to call Next manually before Removing. // after deleting, so we have to call Next manually before Removing.
primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) { primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) {
id := parseKeyFromNamer(v.(primitives.Namer)) id := parseKeyFromNamer(v.(primitives.Namer))
gridMsg := c.message(id.expand()) gridMsg := c.message(id.expand())
if id := gridMsg.ID(); id != "" { state := gridMsg.Unwrap(false)
delete(c.messages, idKey(id))
} if state.ID != "" {
if nonce := gridMsg.Nonce(); nonce != "" { delete(c.messages, idKey(state.ID))
delete(c.messages, nonceKey(nonce))
} }
gridMsg.Row().Destroy() if state.Nonce != "" {
delete(c.messages, nonceKey(state.Nonce))
}
destroyMsg(gridMsg)
n-- n--
return n == 0 return n == 0
@ -519,10 +463,16 @@ func (c *ListStore) HighlightReference(ref markup.ReferenceSegment) {
} }
func (c *ListStore) Highlight(msg MessageRow) { func (c *ListStore) Highlight(msg MessageRow) {
gts.ExecLater(func() { gts.ExecAsync(func() {
row := msg.Row() state := msg.Unwrap(false)
row.GrabFocus() state.Row.GrabFocus()
c.ListBox.DragHighlightRow(row) c.ListBox.DragHighlightRow(state.Row)
gts.DoAfter(2*time.Second, c.ListBox.DragUnhighlightRow) gts.DoAfter(2*time.Second, c.ListBox.DragUnhighlightRow)
}) })
} }
func destroyMsg(row *messageRow) {
state := row.Unwrap(true)
state.Author.Name.Stop()
state.Row.Destroy()
}

View File

@ -272,8 +272,8 @@ var deleteAttBtnCSS = primitives.PrepareCSS(`
func (c *Container) addPreview(name string, thumbnail *cairo.Surface) { func (c *Container) addPreview(name string, thumbnail *cairo.Surface) {
// Make a fallback image first. // Make a fallback image first.
gimg, _ := roundimage.NewImage(4) // border-radius: 4px gimg := roundimage.NewImage(4) // border-radius: 4px
primitives.SetImageIcon(gimg.Image, iconFromName(name), IconSize) primitives.SetImageIcon(&gimg.Image, iconFromName(name), IconSize)
gimg.SetSizeRequest(ThumbSize, ThumbSize) gimg.SetSizeRequest(ThumbSize, ThumbSize)
gimg.SetVAlign(gtk.ALIGN_CENTER) gimg.SetVAlign(gtk.ALIGN_CENTER)
gimg.SetHAlign(gtk.ALIGN_CENTER) gimg.SetHAlign(gtk.ALIGN_CENTER)

View File

@ -6,11 +6,14 @@ 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/log"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"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/username" "github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"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-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/handy" "github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
@ -19,10 +22,12 @@ import (
// Controller is an interface to control message containers. // Controller is an interface to control message containers.
type Controller interface { type Controller interface {
AddPresendMessage(msg PresendMessage) (onErr func(error)) LatestMessageFrom(userID cchat.ID) container.MessageRow
LatestMessageFrom(userID cchat.ID) (messageID cchat.ID, ok bool) MessageAuthor(msgID cchat.ID) *message.Author
MessageAuthor(msgID cchat.ID) cchat.Author Author(authorID cchat.ID) (name rich.LabelStateStorer)
Author(authorID cchat.ID) cchat.Author
// SendMessage asynchronously sends the given message.
SendMessage(msg message.PresendMessage)
} }
// LabelBorrower is an interface that allows the caller to borrow a label. // LabelBorrower is an interface that allows the caller to borrow a label.
@ -333,13 +338,13 @@ func (f *Field) StartReplyingTo(msgID cchat.ID) {
f.sendIcon.SetFromIconName(replyButtonIcon, gtk.ICON_SIZE_BUTTON) f.sendIcon.SetFromIconName(replyButtonIcon, gtk.ICON_SIZE_BUTTON)
if author := f.ctrl.MessageAuthor(msgID); author != nil { if author := f.ctrl.MessageAuthor(msgID); author != nil {
label := author.Name.Label()
// Extract the name from the author's rich text and only render the area // Extract the name from the author's rich text and only render the area
// with the MessageReference. // with the MessageReference.
name := author.Name() for _, seg := range label.Segments {
for _, seg := range name.Segments {
if seg.AsMessageReferencer() != nil || seg.AsMentioner() != nil { if seg.AsMessageReferencer() != nil || seg.AsMentioner() != nil {
mention := markup.Render(markup.SubstringSegment(name, seg)) mention := markup.Render(markup.SubstringSegment(label, seg))
f.indicator.BorrowLabel("Replying to " + mention) f.indicator.BorrowLabel("Replying to " + mention)
return return
} }

View File

@ -48,12 +48,14 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
} }
// Try and find the latest message ID that is ours. // Try and find the latest message ID that is ours.
id, ok := f.ctrl.LatestMessageFrom(f.UserID) msgr := f.ctrl.LatestMessageFrom(f.UserID)
if !ok { if msgr == nil {
// No messages found, so we can passthrough normally. // No messages found, so we can passthrough normally.
return false return false
} }
id := msgr.Unwrap(false).ID
// If we don't support message editing, then passthrough events. // If we don't support message editing, then passthrough events.
if !f.Editable(id) { if !f.Editable(id) {
return false return false

View File

@ -8,10 +8,9 @@ import (
"time" "time"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"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/text" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/twmb/murmur3" "github.com/twmb/murmur3"
) )
@ -63,17 +62,9 @@ func (f *Field) sendInput() {
return return
} }
// Derive the author. Prefer the author of the current user from the message f.ctrl.SendMessage(SendMessageData{
// buffer over the one in the username feed, unless we can't find any.
var author = f.ctrl.Author(f.UserID)
if author == nil {
author = newAuthor(f)
}
f.SendMessage(SendMessageData{
time: time.Now().UTC(), time: time.Now().UTC(),
content: text, content: text,
author: author,
nonce: f.generateNonce(), nonce: f.generateNonce(),
replyID: f.replyingID, replyID: f.replyingID,
files: attachments, files: attachments,
@ -86,19 +77,23 @@ func (f *Field) sendInput() {
f.text.GrabFocus() f.text.GrabFocus()
} }
func (f *Field) SendMessage(data PresendMessage) { // func (f *Field) SendMessage(data message.PresendMessage) {
// presend message into the container through the controller // f.ctrl.SendMessage(data)
var onErr = f.ctrl.AddPresendMessage(data)
// Copy the sender to prevent race conditions. // // message.NewPresendState(f.Username.State, data)
var sender = f.Sender
gts.Async(func() (func(), error) { // // // presend message into the container through the controller
if err := sender.Send(data); err != nil { // // var onErr = f.ctrl.AddPresendMessage(data)
return func() { onErr(err) }, errors.Wrap(err, "Failed to send message")
} // // // Copy the sender to prevent race conditions.
return nil, nil // // var sender = f.Sender
}) // // gts.Async(func() (func(), error) {
} // // if err := sender.Send(data); err != nil {
// // return func() { onErr(err) }, errors.Wrap(err, "Failed to send message")
// // }
// // return nil, nil
// // })
// }
// Files is a list of attachments. // Files is a list of attachments.
type Files []attachment.File type Files []attachment.File
@ -117,56 +112,23 @@ func (files Files) Attachments() []cchat.MessageAttachment {
type SendMessageData struct { type SendMessageData struct {
time time.Time time time.Time
content string content string
author cchat.Author
nonce string nonce string
replyID cchat.ID replyID cchat.ID
files Files files Files
} }
var _ cchat.SendableMessage = (*SendMessageData)(nil) var (
_ cchat.SendableMessage = (*SendMessageData)(nil)
// PresendMessage is an interface for any message about to be sent. _ message.PresendMessage = (*SendMessageData)(nil)
type PresendMessage interface { )
cchat.MessageHeader // returns nonce and time
cchat.SendableMessage
cchat.Noncer
// These methods are reserved for internal use.
Author() cchat.Author
Files() []attachment.File
}
var _ PresendMessage = (*SendMessageData)(nil)
// ID returns a pseudo ID for internal use. // ID returns a pseudo ID for internal use.
func (s SendMessageData) ID() string { return s.nonce } func (s SendMessageData) ID() string { return s.nonce }
func (s SendMessageData) Time() time.Time { return s.time } func (s SendMessageData) Time() time.Time { return s.time }
func (s SendMessageData) Content() string { return s.content } func (s SendMessageData) Content() string { return s.content }
func (s SendMessageData) Author() cchat.Author { return s.author }
func (s SendMessageData) AsNoncer() cchat.Noncer { return s } func (s SendMessageData) AsNoncer() cchat.Noncer { return s }
func (s SendMessageData) Nonce() string { return s.nonce } func (s SendMessageData) Nonce() string { return s.nonce }
func (s SendMessageData) Files() []attachment.File { return s.files } func (s SendMessageData) Files() []attachment.File { return s.files }
func (s SendMessageData) AsAttacher() cchat.Attacher { return s.files } func (s SendMessageData) AsAttacher() cchat.Attacher { return s.files }
func (s SendMessageData) AsReplier() cchat.Replier { return s } func (s SendMessageData) AsReplier() cchat.Replier { return s }
func (s SendMessageData) ReplyingTo() cchat.ID { return s.replyID } func (s SendMessageData) ReplyingTo() cchat.ID { return s.replyID }
type sendableAuthor struct {
id cchat.ID
name text.Rich
avatarURL string
}
func newAuthor(f *Field) sendableAuthor {
return sendableAuthor{
f.UserID,
f.Username.GetLabel(),
f.Username.GetIconURL(),
}
}
var _ cchat.Author = (*sendableAuthor)(nil)
func (a sendableAuthor) ID() string { return a.id }
func (a sendableAuthor) Name() text.Rich { return a.name }
func (a sendableAuthor) Avatar() string { return a.avatarURL }

View File

@ -2,12 +2,14 @@ package username
import ( import (
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/config" "github.com/diamondburned/cchat-gtk/internal/ui/config"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"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/text" "github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
) )
const AvatarSize = 24 const AvatarSize = 24
@ -24,21 +26,20 @@ func init() {
} }
type Container struct { type Container struct {
*gtk.Revealer gtk.Revealer
State *message.Author
main *gtk.Box main *gtk.Box
avatar *rich.Icon avatar *roundimage.Image
label *rich.Label label *rich.Label
} }
var (
_ cchat.LabelContainer = (*Container)(nil)
_ cchat.IconContainer = (*Container)(nil)
)
var usernameCSS = primitives.PrepareCSS(` var usernameCSS = primitives.PrepareCSS(`
.username-view { margin: 0 5px } .username-view { margin: 0 5px }
`) `)
var fallbackAuthor = message.NewCustomAuthor("", text.Plain("self"))
func NewContainer() *Container { func NewContainer() *Container {
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5) box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
box.Show() box.Show()
@ -56,13 +57,32 @@ func NewContainer() *Container {
// thread. // thread.
currentRevealer = rev.SetRevealChild currentRevealer = rev.SetRevealChild
container := Container{ author := message.NewCustomAuthor("", text.Plain("self"))
Revealer: rev,
u := Container{
Revealer: *rev,
State: &author,
main: box, main: box,
} }
container.Reset() u.Reset()
return &container u.avatar = roundimage.NewImage(0)
u.avatar.SetSize(AvatarSize)
u.avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
u.avatar.Show()
rich.BindRoundImage(u.avatar, &u.State.Name, false)
u.label = rich.NewLabel(&u.State.Name)
u.label.SetEllipsize(pango.ELLIPSIZE_END)
u.label.SetMaxWidthChars(35)
u.label.Show()
primitives.RemoveChildren(u.main)
u.main.PackStart(u.avatar, false, false, 0)
u.main.PackStart(u.label, false, false, 0)
return &u
} }
func (u *Container) SetRevealChild(reveal bool) { func (u *Container) SetRevealChild(reveal bool) {
@ -72,75 +92,39 @@ 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().IsEmpty() || u.avatar.URL() != "") && showUser show := false
show = !u.State.Name.Label().IsEmpty()
show = show || u.avatar.GetImageURL() != ""
show = show || showUser
return true
} }
func (u *Container) Reset() { func (u *Container) Reset() {
u.SetRevealChild(false) u.SetRevealChild(false)
u.State.Name.Stop()
u.avatar = rich.NewIcon(AvatarSize)
u.avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
u.avatar.Show()
u.label = rich.NewLabel(text.Rich{})
u.label.SetMaxWidthChars(35)
u.label.Show()
primitives.RemoveChildren(u.main)
u.main.PackStart(u.avatar, false, false, 0)
u.main.PackStart(u.label, false, false, 0)
} }
// Update is not thread-safe. // Update is not thread-safe.
func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) { func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) {
// Set the fallback username. // Set the fallback username.
u.label.SetLabelUnsafe(session.Name()) u.State.Name.BindNamer(u.main, "destroy", session)
// Reveal the name if it's not empty. // Reveal the name if it's not empty.
u.SetRevealChild(true) u.SetRevealChild(true)
// Does messenger implement Nicknamer? If yes, use it. // Does messenger implement Nicknamer? If yes, use it.
if nicknamer := messenger.AsNicknamer(); nicknamer != nil { if nicknamer := messenger.AsNicknamer(); nicknamer != nil {
u.label.AsyncSetLabel(nicknamer.Nickname, "Error fetching server nickname") u.State.Name.BindNamer(u.main, "destroy", nicknamer)
}
// Does session implement an icon? Update if yes.
if iconer := session.AsIconer(); iconer != nil {
u.avatar.AsyncSetIconer(iconer, "Error fetching session icon URL")
} }
} }
// GetLabel is not thread-safe. // Label returns the underlying label.
func (u *Container) GetLabel() text.Rich { func (u *Container) Label() text.Rich {
return u.label.GetLabel() return u.State.Name.Label()
} }
// GetLabelMarkup is not thread-safe. // LabelMarkup returns the underlying label's markup.
func (u *Container) GetLabelMarkup() string { func (u *Container) GetLabelMarkup() string {
return u.label.Label.GetLabel() return u.label.Output().Markup
}
// SetLabel is thread-safe.
func (u *Container) SetLabel(content text.Rich) {
gts.ExecAsync(func() {
u.label.SetLabelUnsafe(content)
// Reveal if the name is not empty.
u.SetRevealChild(true)
})
}
// SetIcon is thread-safe.
func (u *Container) SetIcon(url string) {
gts.ExecAsync(func() {
u.avatar.SetIconUnsafe(url)
// Reveal if the icon URL is not empty. We don't touch anything if the
// URL is empty, as the name might not be.
u.SetRevealChild(true)
})
}
// GetIconURL is not thread-safe.
func (u *Container) GetIconURL() string {
return u.avatar.URL()
} }

View File

@ -3,7 +3,6 @@ package memberlist
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/gts"
@ -128,20 +127,22 @@ func (c *Container) SetSectionsUnsafe(sections []cchat.MemberSection) {
// to this function instead of Reset to not halt for too long. // to this function instead of Reset to not halt for too long.
primitives.RemoveChildren(c.Main) primitives.RemoveChildren(c.Main)
var newSections = make([]*Section, len(sections)) newSections := make([]*Section, len(sections))
oldSections := c.Sections
for i, section := range sections { for i, section := range sections {
sc, ok := c.Sections[section.ID()] sc, ok := c.Sections[section.ID()]
if !ok { if !ok {
sc = NewSection(section, &c.eventQueue) sc = NewSection(section, &c.eventQueue)
} else { } else {
sc.Update(section.Name(), section.Total()) sc.Update(section)
} }
newSections[i] = sc newSections[i] = sc
} }
// Remove all old sections. // Remove all old sections.
for id := range c.Sections { for id := range c.Sections {
delete(c.Sections, id) delete(c.Sections, id)
} }
@ -152,6 +153,16 @@ func (c *Container) SetSectionsUnsafe(sections []cchat.MemberSection) {
c.Sections[section.ID] = section c.Sections[section.ID] = section
} }
// Destroy old sections.
for _, section := range oldSections {
_, notOld := c.Sections[section.ID]
if notOld {
continue
}
section.Destroy()
}
c.ctrl.MemberListUpdated(c) c.ctrl.MemberListUpdated(c)
} }
@ -173,7 +184,7 @@ type Section struct {
ID string ID string
// state // state
name text.Rich name rich.NameContainer
total int total int
Header *rich.Label Header *rich.Label
@ -197,26 +208,40 @@ var sectionBodyCSS = primitives.PrepareClassCSS("section-body", `
`) `)
func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section { func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
header := rich.NewLabel(text.Rich{}) section := &Section{
header.Show() ID: sect.ID(),
sectionHeaderCSS(header) name: rich.NameContainer{},
}
body, _ := gtk.ListBoxNew() section.Header = rich.NewLabel(&section.name)
body.SetSelectionMode(gtk.SELECTION_NONE) section.Header.Show()
body.SetActivateOnSingleClick(true) sectionHeaderCSS(section.Header)
body.SetSortFunc(listSortNameAsc) // A-Z
body.Show()
sectionBodyCSS(body)
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) section.Header.SetRenderer(func(rich text.Rich) markup.RenderOutput {
box.PackStart(header, false, false, 0) out := markup.RenderCmplx(rich)
box.PackStart(body, false, false, 0) if section.total > 0 {
box.Show() out.Markup += fmt.Sprintf("—%d", section.total)
}
return out
})
section.Body, _ = gtk.ListBoxNew()
section.Body.SetSelectionMode(gtk.SELECTION_NONE)
section.Body.SetActivateOnSingleClick(true)
section.Body.SetSortFunc(listSortNameAsc) // A-Z
section.Body.Show()
sectionBodyCSS(section.Body)
section.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
section.Box.PackStart(section.Header, false, false, 0)
section.Box.PackStart(section.Body, false, false, 0)
section.Box.Show()
var members = map[string]*Member{} var members = map[string]*Member{}
// On row click, show the mention popup if any. // On row click, show the mention popup if any.
body.Connect("row-activated", func(_ *gtk.ListBox, r *gtk.ListBoxRow) { section.Body.Connect("row-activated", func(_ *gtk.ListBox, r *gtk.ListBoxRow) {
var i = r.GetIndex() var i = r.GetIndex()
// Cold path; we can afford searching in the map. // Cold path; we can afford searching in the map.
for _, member := range members { for _, member := range members {
@ -226,32 +251,20 @@ func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
} }
}) })
section := &Section{ section.name.QueueNamer(context.Background(), sect)
ID: sect.ID(), section.Header.Connect("destroy", section.name.Stop)
Box: box,
Header: header,
Body: body,
Members: members,
}
section.Update(sect.Name(), sect.Total())
return section return section
} }
func (s *Section) Update(name text.Rich, total int) { func (s *Section) Destroy() {
s.name = name s.name.Stop()
s.total = total s.Box.Destroy()
}
var content = s.name.Content func (s *Section) Update(sect cchat.MemberSection) {
if total > 0 { s.total = sect.Total()
content += fmt.Sprintf("—%d", total) s.name.QueueNamer(context.Background(), sect)
}
s.Header.SetLabelUnsafe(text.Rich{
Content: content,
Segments: s.name.Segments,
})
} }
func (s *Section) SetMember(member cchat.ListMember) { func (s *Section) SetMember(member cchat.ListMember) {
@ -292,14 +305,16 @@ type Member struct {
*gtk.ListBoxRow *gtk.ListBoxRow
Main *gtk.Box Main *gtk.Box
Avatar *rich.Icon Avatar *roundimage.StillImage
Name *gtk.Label Name *rich.Label
output markup.RenderOutput
name rich.LabelState
second text.Rich
status cchat.Status
parent *gtk.ListBox parent *gtk.ListBox
} }
const AvatarSize = 34 const AvatarSize = 32
var memberRowCSS = primitives.PrepareClassCSS("member-row", ` var memberRowCSS = primitives.PrepareClassCSS("member-row", `
.member-row { .member-row {
@ -320,46 +335,61 @@ var avatarMemberCSS = primitives.PrepareClassCSS("avatar-member", `
`) `)
func NewMember(member cchat.ListMember) *Member { func NewMember(member cchat.ListMember) *Member {
m := Member{}
evb, _ := gtk.EventBoxNew() evb, _ := gtk.EventBoxNew()
evb.AddEvents(int(gdk.EVENT_ENTER_NOTIFY) | int(gdk.EVENT_LEAVE_NOTIFY)) evb.AddEvents(int(gdk.EVENT_ENTER_NOTIFY) | int(gdk.EVENT_LEAVE_NOTIFY))
evb.Show() evb.Show()
img, _ := roundimage.NewStaticImage(evb, 0) m.Avatar = roundimage.NewStillImage(evb, 9999)
img.Show() m.Avatar.SetSize(AvatarSize)
m.Avatar.SetPlaceholderIcon("user-info-symbolic", AvatarSize)
m.Avatar.Show()
avatarMemberCSS(m.Avatar)
icon := rich.NewCustomIcon(img, AvatarSize) rich.BindRoundImage(m.Avatar, &m.name, true)
icon.SetPlaceholderIcon("user-info-symbolic", AvatarSize)
icon.Show()
avatarMemberCSS(icon)
lbl, _ := gtk.LabelNew("") m.Name = rich.NewLabel(&m.name)
lbl.SetUseMarkup(true) m.Name.SetUseMarkup(true)
lbl.SetXAlign(0) m.Name.SetXAlign(0)
lbl.SetEllipsize(pango.ELLIPSIZE_END) m.Name.SetEllipsize(pango.ELLIPSIZE_END)
lbl.Show() m.Name.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) m.Name.SetRenderer(func(rich text.Rich) markup.RenderOutput {
box.PackStart(icon, false, false, 0) out := markup.RenderCmplx(rich)
box.PackStart(lbl, true, true, 0)
box.Show()
memberBoxCSS(box)
evb.Add(box) if m.status != cchat.StatusUnknown {
out.Markup = fmt.Sprintf(
`<span color="#%06X" size="large">●</span> %s`,
statusColors(member.Status()), out.Markup,
)
}
r, _ := gtk.ListBoxRowNew() if !m.second.IsEmpty() {
memberRowCSS(r) out.Markup += fmt.Sprintf(
r.Add(evb) "\n"+`<span alpha="85%%"><sup>%s</sup></span>`,
markup.Render(m.second),
)
}
m := &Member{ return out
ListBoxRow: r, })
Main: box,
Avatar: icon, m.Main, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
Name: lbl, m.Main.PackStart(m.Avatar, false, false, 0)
} m.Main.PackStart(m.Name, true, true, 0)
m.Main.Show()
memberBoxCSS(m.Main)
evb.Add(m.Main)
m.ListBoxRow, _ = gtk.ListBoxRowNew()
m.ListBoxRow.Add(evb)
memberRowCSS(m.ListBoxRow)
m.Update(member) m.Update(member)
return m return &m
} }
var noMentionLinks = markup.RenderConfig{ var noMentionLinks = markup.RenderConfig{
@ -368,38 +398,22 @@ var noMentionLinks = markup.RenderConfig{
} }
func (m *Member) Update(member cchat.ListMember) { func (m *Member) Update(member cchat.ListMember) {
m.status = member.Status()
m.second = member.Secondary()
m.name.SetLabel(member.Name())
m.ListBoxRow.SetName(member.Name().Content) m.ListBoxRow.SetName(member.Name().Content)
if iconer := member.AsIconer(); iconer != nil {
m.Avatar.AsyncSetIconer(iconer, "Failed to get member list icon")
}
m.output = markup.RenderCmplxWithConfig(member.Name(), noMentionLinks)
txt := strings.Builder{}
txt.WriteString(fmt.Sprintf(
`<span color="#%06X" size="large">●</span> %s`,
statusColors(member.Status()), m.output.Markup,
))
if bot := member.Secondary(); !bot.IsEmpty() {
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. // Popup pops up the mention popover if any.
func (m *Member) Popup(evq EventQueuer) { func (m *Member) Popup(evq EventQueuer) {
if len(m.output.Mentions) == 0 { out := m.Name.Output()
if len(out.Mentions) == 0 {
return return
} }
p := labeluri.NewPopoverMentioner(m, m.output.Input, m.output.Mentions[0]) p := labeluri.NewPopoverMentioner(m, out.Input, out.Mentions[0])
if p == nil { if p == nil {
return return
} }

View File

@ -1,50 +1,36 @@
package message package message
import ( import (
"context"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
) )
// Author implements cchat.Author. It effectively contains a copy of // Author implements cchat.Author. It effectively contains a copy of
// cchat.Author. // cchat.Author.
type Author struct { type Author struct {
id cchat.ID ID cchat.ID
name text.Rich Name rich.NameContainer
avatarURL string
} }
var _ cchat.Author = (*Author)(nil)
// NewAuthor creates a new Author that is a copy of the given author. // NewAuthor creates a new Author that is a copy of the given author.
func NewAuthor(author cchat.Author) Author { func NewAuthor(author cchat.User) Author {
a := Author{} a := Author{ID: author.ID()}
a.Update(author) a.Name.QueueNamer(context.Background(), author)
return a return a
} }
// NewCustomAuthor creates a new Author from the given parameters. // NewCustomAuthor creates a new Author from the given parameters.
func NewCustomAuthor(id cchat.ID, name text.Rich, avatar string) Author { func NewCustomAuthor(id cchat.ID, name text.Rich) Author {
return Author{ return Author{
id, ID: id,
name, Name: rich.NameContainer{LabelState: *rich.NewLabelState(name)},
avatar,
} }
} }
func (a *Author) Update(author cchat.Author) { // Update sets a new name.
a.id = author.ID() func (author *Author) Update(user cchat.Namer) {
a.name = author.Name() author.Name.QueueNamer(context.Background(), user)
a.avatarURL = author.Avatar()
}
func (a Author) ID() string {
return a.id
}
func (a Author) Name() text.Rich {
return a.name
}
func (a Author) Avatar() string {
return a.avatarURL
} }

View File

@ -1,6 +1,7 @@
package message package message
import ( import (
"context"
"time" "time"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
@ -8,70 +9,65 @@ import (
"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/rich" "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/labeluri"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango" "github.com/gotk3/gotk3/pango"
) )
// Container describes a message container that wraps a state. These methods are
// made for containers to override; methods not meant to be override are not
// exposed and will be done directly on the State.
type Container interface { type Container interface {
ID() cchat.ID // Unwrap unwraps the message container and, if revert is true, revert the
Time() time.Time // state to a clean version. Containers must implement this method by
Author() cchat.Author // itself.
Nonce() string Unwrap(revert bool) *State
UpdateAuthor(cchat.Author) // UpdateContent updates the underlying content widget.
UpdateContent(c text.Rich, edited bool) UpdateContent(content text.Rich, edited bool)
UpdateTimestamp(time.Time)
// SetReferenceHighlighter sets the reference highlighter into the message.
SetReferenceHighlighter(refer labeluri.ReferenceHighlighter)
} }
// FillContainer sets the container's contents to the one from MessageCreate. // State provides a single generic message container for subpackages
func FillContainer(c Container, msg cchat.MessageCreate) {
c.UpdateAuthor(msg.Author())
c.UpdateContent(msg.Content(), false)
c.UpdateTimestamp(msg.Time())
}
// RefreshContainer sets the container's contents to the one from
// GenericContainer. This is mainly used for transferring between different
// containers.
func RefreshContainer(c Container, gc *GenericContainer) {
c.UpdateTimestamp(gc.time)
}
// GenericContainer provides a single generic message container for subpackages
// to use. // to use.
type GenericContainer struct { type State struct {
*gtk.Box gtk.Box
row *gtk.ListBoxRow // contains Box Row *gtk.ListBoxRow // contains Box
class string class string
id string ID cchat.ID
time time.Time Time time.Time
author Author Nonce string
nonce string Author *Author
Content *gtk.Box Content *gtk.Box
ContentBody *labeluri.Label ContentBody *labeluri.Label
ContentBodyStyle *gtk.StyleContext ContentBodyStyle *gtk.StyleContext
menuItems []menu.Item MenuItems []menu.Item
} }
var _ Container = (*GenericContainer)(nil) // NewState creates a new message state with the given ID and nonce. It does not
// update the widgets, so FillContainer should be called afterwards.
func NewState(msg cchat.MessageCreate) *State {
author := msg.Author()
// NewContainer creates a new message container with the given ID and nonce. It c := NewEmptyState()
// does not update the widgets, so FillContainer should be called afterwards. c.Author.ID = author.ID()
func NewContainer(msg cchat.MessageCreate) *GenericContainer { c.Author.Name.QueueNamer(context.Background(), author)
c := NewEmptyContainer() c.ID = msg.ID()
c.id = msg.ID() c.Time = msg.Time()
c.time = msg.Time() c.Nonce = msg.Nonce()
c.nonce = msg.Nonce()
c.author.Update(msg.Author())
return c return c
} }
func NewEmptyContainer() *GenericContainer { // NewEmptyState creates a new empty message state. The author should be set
// immediately afterwards; it is invalid once the state is used.
func NewEmptyState() *State {
ctbody := labeluri.NewLabel(text.Rich{}) ctbody := labeluri.NewLabel(text.Rich{})
ctbody.SetVExpand(true) ctbody.SetVExpand(true)
ctbody.SetHAlign(gtk.ALIGN_START) ctbody.SetHAlign(gtk.ALIGN_START)
@ -100,9 +96,10 @@ func NewEmptyContainer() *GenericContainer {
row.Show() row.Show()
primitives.AddClass(row, "message-row") primitives.AddClass(row, "message-row")
gc := &GenericContainer{ gc := &State{
Box: box, Box: *box,
row: row, Row: row,
Author: &Author{},
Content: ctbox, Content: ctbox,
ContentBody: ctbody, ContentBody: ctbody,
@ -110,81 +107,49 @@ func NewEmptyContainer() *GenericContainer {
// Time is important, as it is used to sort messages, so we have to be // Time is important, as it is used to sort messages, so we have to be
// careful with this. // careful with this.
time: time.Now(), Time: time.Now(),
} }
// This may either work, or it may cause memory leaks.
row.Connect("destroy", func() { gc.Author.Name.Stop() })
// Bind the custom popup menu to the content label. // Bind the custom popup menu to the content label.
gc.ContentBody.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) { gc.ContentBody.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) {
menu.MenuSeparator(m) menu.MenuSeparator(m)
menu.MenuItems(m, gc.menuItems) menu.MenuItems(m, gc.MenuItems)
}) })
return gc return gc
} }
// Row returns the internal list box row. It is used to satisfy MessageRow.
func (m *GenericContainer) Row() *gtk.ListBoxRow { return m.row }
// SetClass sets the internal row's class. // SetClass sets the internal row's class.
func (m *GenericContainer) SetClass(class string) { func (m *State) SetClass(class string) {
if m.class != "" { if m.class != "" {
primitives.RemoveClass(m.row, m.class) primitives.RemoveClass(m.Row, m.class)
} }
primitives.AddClass(m.row, class) primitives.AddClass(m.Row, class)
m.class = class m.class = class
} }
// SetReferenceHighlighter sets the reference highlighter into the message. // SetReferenceHighlighter sets the reference highlighter into the message.
func (m *GenericContainer) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) { func (m *State) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
m.ContentBody.SetReferenceHighlighter(r) m.ContentBody.SetReferenceHighlighter(r)
} }
func (m *GenericContainer) ID() string { // UpdateContent replaces the internal content and the widget.
return m.id func (m *State) UpdateContent(content text.Rich, edited bool) {
} m.ContentBody.SetLabel(content)
func (m *GenericContainer) Time() time.Time {
return m.time
}
func (m *GenericContainer) Author() cchat.Author {
return m.author
}
func (m *GenericContainer) Nonce() string {
return m.nonce
}
func (m *GenericContainer) UpdateTimestamp(t time.Time) {
m.time = t
}
func (m *GenericContainer) UpdateAuthor(author cchat.Author) {
m.author.Update(author)
}
func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
m.ContentBody.SetLabelUnsafe(content)
if edited { if edited {
markup := m.ContentBody.Output().Markup m.ContentBody.SetRenderer(func(content text.Rich) markup.RenderOutput {
markup += " " + rich.Small("(edited)") output := markup.RenderCmplx(content)
m.ContentBody.SetMarkup(markup) output.Markup += rich.Small(text.Plain("(edited)")).Markup
return output
})
} }
} }
// AttachMenu connects signal handlers to handle a list of menu items from func (m *State) Focusable() gtk.IWidget {
// the container.
func (m *GenericContainer) AttachMenu(newItems []menu.Item) {
m.menuItems = newItems
}
// MenuItems returns the list of menu items for this message.
func (m *GenericContainer) MenuItems() []menu.Item {
return m.menuItems
}
func (m *GenericContainer) Focusable() gtk.IWidget {
return m.Content return m.Content
} }

View File

@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"html" "html"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/humanize" "github.com/diamondburned/cchat-gtk/internal/humanize"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"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/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
@ -16,33 +16,51 @@ var EmptyContentPlaceholder = fmt.Sprintf(
`<span alpha="25%%">%s</span>`, html.EscapeString("<empty>"), `<span alpha="25%%">%s</span>`, html.EscapeString("<empty>"),
) )
type PresendContainer interface { // Presender describes actions doable on a presend message container.
SetDone(id string) type Presender interface {
SendingMessage() PresendMessage
SetDone(id cchat.ID)
SetLoading() SetLoading()
SetSentError(err error) SetSentError(err error)
} }
// PresendGenericContainer is the generic container with extra methods // PresendMessage is an interface for any message about to be sent.
// implemented for stateful mutability of the generic message container. type PresendMessage interface {
type GenericPresendContainer struct { cchat.MessageHeader
*GenericContainer cchat.SendableMessage
cchat.Noncer
// These methods are reserved for internal use.
Files() []attachment.File
}
// PresendState is the generic state with extra methods implemented for stateful
// mutability of the generic message state.
type PresendState struct {
*State
// states; to be cleared on SetDone() // states; to be cleared on SetDone()
presend input.PresendMessage presend PresendMessage
uploads *attachment.MessageUploader uploads *attachment.MessageUploader
} }
var _ PresendContainer = (*GenericPresendContainer)(nil) var (
_ Presender = (*PresendState)(nil)
)
func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer { type SendMessageData struct {
c := NewEmptyContainer() }
c.nonce = msg.Nonce()
c.UpdateAuthor(msg.Author())
c.UpdateTimestamp(msg.Time())
p := &GenericPresendContainer{ // NewPresendState creates a new presend state.
GenericContainer: c, func NewPresendState(self *Author, msg PresendMessage) *PresendState {
c := NewEmptyState()
c.Author = self
c.Nonce = msg.Nonce()
c.Time = msg.Time()
p := &PresendState{
State: c,
presend: msg, presend: msg,
uploads: attachment.NewMessageUploader(msg.Files()), uploads: attachment.NewMessageUploader(msg.Files()),
} }
@ -51,14 +69,18 @@ func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
return p return p
} }
func (m *GenericPresendContainer) SetSensitive(sensitive bool) { func (m *PresendState) SendingMessage() PresendMessage { return m.presend }
// SetSensitive sets the sensitivity of the content.
func (m *PresendState) SetSensitive(sensitive bool) {
m.Content.SetSensitive(sensitive) m.Content.SetSensitive(sensitive)
} }
func (m *GenericPresendContainer) SetDone(id string) { // SetDone sets the status of the state.
func (m *PresendState) SetDone(id cchat.ID) {
// Apply the received ID. // Apply the received ID.
m.id = id m.ID = id
m.nonce = "" m.Nonce = ""
// Reset the state to be normal. Especially setting presend to nil should // Reset the state to be normal. Especially setting presend to nil should
// free it from memory. // free it from memory.
@ -76,7 +98,8 @@ func (m *GenericPresendContainer) SetDone(id string) {
m.SetSensitive(true) m.SetSensitive(true)
} }
func (m *GenericPresendContainer) SetLoading() { // SetLoading greys the message to indicate that it's loading.
func (m *PresendState) SetLoading() {
m.SetSensitive(false) m.SetSensitive(false)
m.Content.SetTooltipText("") m.Content.SetTooltipText("")
@ -100,7 +123,8 @@ func (m *GenericPresendContainer) SetLoading() {
} }
} }
func (m *GenericPresendContainer) SetSentError(err error) { // SetSentError sets the error into the message to notify the user.
func (m *PresendState) SetSentError(err error) {
m.SetSensitive(true) // allow events incl right clicks m.SetSensitive(true) // allow events incl right clicks
m.Content.SetTooltipText(err.Error()) m.Content.SetTooltipText(err.Error())
@ -132,6 +156,6 @@ func (m *GenericPresendContainer) SetSentError(err error) {
} }
// clearBox clears everything inside the content container. // clearBox clears everything inside the content container.
func (m *GenericPresendContainer) clearBox() { func (m *PresendState) clearBox() {
primitives.RemoveChildren(m.Content) primitives.RemoveChildren(m.Content)
} }

View File

@ -84,11 +84,11 @@ func (mc *MessageControl) Enable(msg container.MessageRow, names MessageItemName
mc.SetSensitive(true) mc.SetSensitive(true)
mc.SetRevealChild(true && !mc.hide) mc.SetRevealChild(true && !mc.hide)
items := msg.MenuItems() unwrap := msg.Unwrap(false)
mc.Reply.bind(menu.FindItemFunc(items, names.Reply)) mc.Reply.bind(menu.FindItemFunc(unwrap.MenuItems, names.Reply))
mc.Edit.bind(menu.FindItemFunc(items, names.Edit)) mc.Edit.bind(menu.FindItemFunc(unwrap.MenuItems, names.Edit))
mc.Delete.bind(menu.FindItemFunc(items, names.Delete)) mc.Delete.bind(menu.FindItemFunc(unwrap.MenuItems, names.Delete))
} }
// SetHidden sets whether or not the control should be hidden. // SetHidden sets whether or not the control should be hidden.

View File

@ -1,17 +1,25 @@
package typing package typing
import ( import (
"context"
"sort" "sort"
"time" "time"
"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/ui/rich"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type typer struct {
cchat.User
s *rich.NameContainer
t time.Time
}
type State struct { type State struct {
// states // states
typers []cchat.Typer typers []typer
timeout time.Duration timeout time.Duration
canceler func() canceler func()
invalidated bool invalidated bool
@ -67,35 +75,54 @@ func (s *State) loop() {
// Call the event handler if things are invalidated. // Call the event handler if things are invalidated.
if s.invalidated { if s.invalidated {
s.changed(s, len(s.typers) == 0) s.update()
s.invalidated = false s.invalidated = false
} }
} }
// update force-runs th callback.
func (s *State) update() {
s.changed(s, len(s.typers) == 0)
}
// invalidate sorts and invalidates the state. // invalidate sorts and invalidates the state.
func (s *State) invalidate() { func (s *State) invalidate() {
// Sort the list of typers again. // Sort the list of typers again.
sort.Slice(s.typers, func(i, j int) bool { sort.Slice(s.typers, func(i, j int) bool {
return s.typers[i].Time().Before(s.typers[j].Time()) return s.typers[i].t.Before(s.typers[j].t)
}) })
s.invalidated = true s.invalidated = true
} }
// AddTyper is thread-safe. // AddTyper is thread-safe.
func (s *State) AddTyper(typer cchat.Typer) { func (s *State) AddTyper(user cchat.User) {
now := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
state := rich.NameContainer{}
state.QueueNamer(ctx, user)
gts.ExecAsync(func() { gts.ExecAsync(func() {
defer cancel()
defer s.invalidate() defer s.invalidate()
// If the typer already exists, then pop them to the start of the list. // If the typer already exists, then pop them to the start of the list.
for i, t := range s.typers { for i, t := range s.typers {
if t.ID() == typer.ID() { if t.ID() == user.ID() {
s.typers[i] = t s.typers[i] = t
return return
} }
} }
s.typers = append(s.typers, typer) state.OnUpdate(s.update)
s.typers = append(s.typers, typer{
User: user,
s: &state,
t: now,
})
}) })
} }
@ -108,19 +135,24 @@ func (s *State) removeTyper(typerID string) {
defer s.invalidate() defer s.invalidate()
for i, t := range s.typers { for i, t := range s.typers {
if t.ID() == typerID { if t.ID() != typerID {
// Remove the quick way. Sort will take care of ordering. continue
l := len(s.typers) - 1
s.typers[i] = s.typers[l]
s.typers[l] = nil
s.typers = s.typers[:l]
return
} }
// Invalidate the typer's label state.
t.s.Stop()
// Remove the quick way. Sort will take care of ordering.
l := len(s.typers) - 1
s.typers[i] = s.typers[l]
s.typers[l] = typer{}
s.typers = s.typers[:l]
return
} }
} }
func filterTypers(typers []cchat.Typer, timeout time.Duration) ([]cchat.Typer, bool) { func filterTypers(typers []typer, timeout time.Duration) ([]typer, bool) {
// Fast path. // Fast path.
if len(typers) == 0 || timeout == 0 { if len(typers) == 0 || timeout == 0 {
return nil, false return nil, false
@ -130,14 +162,15 @@ func filterTypers(typers []cchat.Typer, timeout time.Duration) ([]cchat.Typer, b
var cut int var cut int
for _, t := range typers { for _, t := range typers {
if now.Sub(t.Time()) < timeout { if now.Sub(t.t) < timeout {
typers[cut] = t typers[cut] = t
cut++ cut++
} }
} }
for i := cut; i < len(typers); i++ { for i := cut; i < len(typers); i++ {
typers[i] = nil typers[i].s.Stop()
typers[i] = typer{}
} }
var changed = cut != len(typers) var changed = cut != len(typers)

View File

@ -137,8 +137,8 @@ func (c *Container) Unborrow() {
} }
} }
func (c *Container) RemoveAuthor(author cchat.Author) { func (c *Container) RemoveAuthor(userID cchat.ID) {
c.state.removeTyper(author.ID()) c.state.removeTyper(userID)
} }
func (c *Container) TrySubscribe(svmsg cchat.Messenger) bool { func (c *Container) TrySubscribe(svmsg cchat.Messenger) bool {
@ -155,7 +155,7 @@ var noMentionLinks = markup.RenderConfig{
NoMentionLinks: true, NoMentionLinks: true,
} }
func render(typers []cchat.Typer) string { func render(typers []typer) string {
// fast path // fast path
if len(typers) == 0 { if len(typers) == 0 {
return "" return ""
@ -164,7 +164,7 @@ func render(typers []cchat.Typer) string {
var builder strings.Builder var builder strings.Builder
for i, typer := range typers { for i, typer := range typers {
output := markup.RenderCmplxWithConfig(typer.Name(), noMentionLinks) output := markup.RenderCmplxWithConfig(typer.s.Label(), noMentionLinks)
builder.WriteString("<b>") builder.WriteString("<b>")
builder.WriteString(output.Markup) builder.WriteString(output.Markup)

View File

@ -15,12 +15,16 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/cozy" "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/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/memberlist" "github.com/diamondburned/cchat-gtk/internal/ui/messages/memberlist"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/sadface" "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/messages/typing"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"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/drag" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag"
"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/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse" "github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
"github.com/diamondburned/handy" "github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
@ -280,7 +284,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 cchat.Server, bc traverse.Breadcrumber) { func (v *View) JoinServer(ses *session.Row, srv *server.ServerRow, bc traverse.Breadcrumber) {
// Set the screen to loading. // Set the screen to loading.
v.FaceView.SetLoading() v.FaceView.SetLoading()
v.ctrl.OnMessageBusy() v.ctrl.OnMessageBusy()
@ -289,19 +293,22 @@ func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc travers
v.reset() v.reset()
// Get the messenger once. // Get the messenger once.
var messenger = server.AsMessenger() var messenger = srv.Server.AsMessenger()
// Exit if this server is not a messenger. // Exit if this server is not a messenger.
if messenger == nil { if messenger == nil {
return return
} }
// Bind the state. // Bind the state.
v.state.bind(session, server, messenger) v.state.bind(ses.Session, srv.Server, messenger)
// 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.SetMessenger(session, messenger) v.InputView.SetMessenger(ses.Session, messenger)
// Bind the container's self user to what we just set.
v.Container.SetSelf(v.InputView.Username.State)
go func() { go func() {
// 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
@ -363,79 +370,84 @@ func (v *View) FetchBacklog() {
v.Container.Highlight(firstMsg) v.Container.Highlight(firstMsg)
} }
firstID := firstMsg.Unwrap(false).ID
gts.Async(func() (func(), error) { gts.Async(func() (func(), error) {
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second) ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
defer cancel() defer cancel()
err := backlogger.Backlog(ctx, firstMsg.ID(), v.Container) err := backlogger.Backlog(ctx, firstID, v.Container)
return done, errors.Wrap(err, "Failed to get messages before ID") return done, errors.Wrap(err, "Failed to get messages before ID")
}) })
} }
func (v *View) AddPresendMessage(msg input.PresendMessage) func(error) {
var presend = v.Container.AddPresendMessage(msg)
return func(err error) {
// Set the retry message.
presend.SetSentError(err)
// Only attach the menu once. Further retries do not need to be
// reattached.
presend.AttachMenu([]menu.Item{
menu.SimpleItem("Retry", func() {
presend.SetLoading()
v.retryMessage(msg, presend)
}),
})
}
}
// AuthorEvent should be called on message create/update/delete. // AuthorEvent should be called on message create/update/delete.
func (v *View) AuthorEvent(author cchat.Author) { func (v *View) AuthorEvent(authorID cchat.ID) {
// 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 authorID != "" {
v.Typing.RemoveAuthor(author) v.Typing.RemoveAuthor(authorID)
} }
} }
// MessageAuthor returns the author from the message with the given ID. // MessageAuthor returns the author from the message with the given ID.
func (v *View) MessageAuthor(msgID cchat.ID) cchat.Author { func (v *View) MessageAuthor(msgID cchat.ID) *message.Author {
msg := v.Container.Message(msgID, "") msg := v.Container.Message(msgID, "")
if msg == nil { if msg == nil {
return nil return nil
} }
return msg.Author() return msg.Unwrap(false).Author
} }
// Author returns the author from the message list with the given author ID. // Author returns the author from the message list with the given author ID.
func (v *View) Author(authorID cchat.ID) cchat.Author { func (v *View) Author(authorID cchat.ID) rich.LabelStateStorer {
msg := v.Container.FindMessage(func(msg container.MessageRow) bool { msg := v.Container.FindMessage(func(msg container.MessageRow) bool {
return msg.Author().ID() == authorID return msg.Unwrap(false).Author.ID == authorID
}) })
if msg == nil { if msg == nil {
return nil return nil
} }
return msg.Author() state := msg.Unwrap(false)
return &state.Author.Name
} }
// LatestMessageFrom returns the last message ID with that author. // LatestMessageFrom returns the last message ID with that author.
func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) { func (v *View) LatestMessageFrom(userID cchat.ID) container.MessageRow {
return v.Container.LatestMessageFrom(userID) return container.LatestMessageFrom(v.Container, userID)
}
func (v *View) SendMessage(msg message.PresendMessage) {
state := message.NewPresendState(v.InputView.Username.State, msg)
msgr := v.Container.NewPresendMessage(state)
v.retryMessage(msgr)
} }
// retryMessage sends the message. // retryMessage sends the message.
func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendMessageRow) { func (v *View) retryMessage(presend container.PresendMessageRow) {
var sender = v.InputView.Sender var sender = v.InputView.Sender
if sender == nil { if sender == nil {
return return
} }
go func() { go func() {
if err := sender.Send(msg); err != nil { if err := sender.Send(presend.SendingMessage()); 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() {
// Set the retry message.
presend.SetSentError(err)
// Only attach the menu once. Further retries do not need to be
// reattached.
state := presend.Unwrap(false)
state.MenuItems = []menu.Item{
menu.SimpleItem("Retry", func() {
presend.SetLoading()
v.retryMessage(presend)
}),
}
})
} }
}() }()
} }
@ -449,33 +461,35 @@ var messageItemNames = MessageItemNames{
// BindMenu attaches the menu constructor into the message with the needed // BindMenu attaches the menu constructor into the message with the needed
// states and callbacks. // states and callbacks.
func (v *View) BindMenu(msg container.MessageRow) { func (v *View) BindMenu(msg container.MessageRow) {
state := msg.Unwrap(false)
// Add 1 for the edit menu item. // Add 1 for the edit menu item.
var mitems = []menu.Item{ var mitems = []menu.Item{
menu.SimpleItem( menu.SimpleItem(
"Reply", func() { v.InputView.StartReplyingTo(msg.ID()) }, "Reply", func() { v.InputView.StartReplyingTo(state.ID) },
), ),
} }
// Do we have editing capabilities? If yes, append a button to allow it. // Do we have editing capabilities? If yes, append a button to allow it.
if v.InputView.Editable(msg.ID()) { if v.InputView.Editable(state.ID) {
mitems = append(mitems, menu.SimpleItem( mitems = append(mitems, menu.SimpleItem(
"Edit", func() { v.InputView.StartEditing(msg.ID()) }, "Edit", func() { v.InputView.StartEditing(state.ID) },
)) ))
} }
// 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.Actions(msg.ID()) var actions = v.actioner.Actions(state.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 {
items[i] = v.makeActionItem(action, msg.ID()) items[i] = v.makeActionItem(action, state.ID)
} }
mitems = append(mitems, items...) mitems = append(mitems, items...)
} }
msg.AttachMenu(mitems) state.MenuItems = mitems
} }
// makeActionItem creates a new menu callback that's called on menu item // makeActionItem creates a new menu callback that's called on menu item

View File

@ -189,7 +189,7 @@ func (c *Completer) update() []gtk.IWidget {
lbox.Show() lbox.Show()
// Label for the primary text. // Label for the primary text.
l := rich.NewLabel(entry.Text) l := rich.NewStaticLabel(entry.Text)
l.Show() l.Show()
lbox.PackStart(l, false, false, 0) lbox.PackStart(l, false, false, 0)
@ -198,7 +198,7 @@ func (c *Completer) update() []gtk.IWidget {
if !entry.Secondary.IsEmpty() { if !entry.Secondary.IsEmpty() {
size = ImageLarge size = ImageLarge
s := rich.NewLabel(text.Rich{}) s := rich.NewStaticLabel(text.Plain(""))
s.SetMarkup(fmt.Sprintf( s.SetMarkup(fmt.Sprintf(
`<span alpha="50%%" size="small">%s</span>`, `<span alpha="50%%" size="small">%s</span>`,
markup.Render(entry.Secondary), markup.Render(entry.Secondary),

View File

@ -1,7 +1,6 @@
package primitives package primitives
import ( import (
"context"
"runtime/debug" "runtime/debug"
"github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/gts"
@ -179,27 +178,13 @@ func NewImageIconPx(icon string, sizepx int) *gtk.Image {
} }
type ImageIconSetter interface { type ImageIconSetter interface {
SetProperty(name string, value interface{}) error SetFromIconName(string, gtk.IconSize)
GetSizeRequest() (w, h int) SetPixelSize(int)
SetSizeRequest(w, h int)
} }
func SetImageIcon(img ImageIconSetter, icon string, sizepx int) { func SetImageIcon(img ImageIconSetter, icon string, sizepx int) {
// Prioritize SetSize() img.SetFromIconName(icon, gtk.ICON_SIZE_BUTTON)
if setter, ok := img.(interface{ SetSize(int) }); ok { img.SetPixelSize(sizepx)
setter.SetSize(sizepx)
} else {
img.SetProperty("pixel-size", sizepx)
}
// Prioritize SetIconName().
if setter, ok := img.(interface{ SetIconName(string) }); ok {
setter.SetIconName(icon)
} else {
img.SetProperty("icon-name", icon)
}
img.SetSizeRequest(sizepx, sizepx)
} }
func PrependMenuItems(menu interface{ Prepend(gtk.IMenuItem) }, items []gtk.IMenuItem) { func PrependMenuItems(menu interface{ Prepend(gtk.IMenuItem) }, items []gtk.IMenuItem) {
@ -240,12 +225,6 @@ type Connector interface {
var _ Connector = (*glib.Object)(nil) var _ Connector = (*glib.Object)(nil)
func HandleDestroyCtx(ctx context.Context, connector Connector) context.Context {
ctx, cancel := context.WithCancel(ctx)
connector.Connect("destroy", func(interface{}) { cancel() })
return ctx
}
func OnRightClick(connector Connector, fn func()) { func OnRightClick(connector Connector, fn func()) {
connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) { connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) {
if gts.EventIsRightClick(ev) { if gts.EventIsRightClick(ev) {

View File

@ -28,6 +28,7 @@ type Avatar struct {
pixbuf *gdk.Pixbuf pixbuf *gdk.Pixbuf
url string url string
size int size int
cancel context.CancelFunc
} }
// Make a better API that allows scaling. // Make a better API that allows scaling.
@ -49,6 +50,7 @@ func NewAvatar(size int) *Avatar {
return &avatar return &avatar
} }
// GetSizeRequest returns the virtual size.
func (a *Avatar) GetSizeRequest() (int, int) { func (a *Avatar) GetSizeRequest() (int, int) {
return a.size, a.size return a.size, a.size
} }
@ -66,7 +68,6 @@ func (a *Avatar) SetSizeRequest(w, h int) {
} }
func (a *Avatar) loadFunc(size int) *gdk.Pixbuf { func (a *Avatar) loadFunc(size int) *gdk.Pixbuf {
// No URL, draw nothing.
if a.url == "" { if a.url == "" {
return nil return nil
} }
@ -75,44 +76,54 @@ func (a *Avatar) loadFunc(size int) *gdk.Pixbuf {
return a.pixbuf return a.pixbuf
} }
// Refetch and rescale.
a.size = size a.size = size
// Technically, this will recurse. However, we're changing the size, so a.refetch()
// eventually it should stop.
httputil.AsyncImage(context.Background(), a, a.url)
if a.pixbuf == nil { return nil
return nil }
func (a *Avatar) refetch() {
a.cancelCtx()
ctx, cancel := context.WithCancel(context.Background())
a.cancel = cancel
httputil.AsyncImage(ctx, a, a.url)
}
func (a *Avatar) cancelCtx() {
if a.cancel != nil {
a.cancel()
a.cancel = nil
} }
// // Temporarily resize for now.
// p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER)
// if err != nil {
// p = a.pixbuf
// }
return a.pixbuf
} }
// SetRadius is a no-op. // SetRadius is a no-op.
func (a *Avatar) SetRadius(float64) {} func (a *Avatar) SetRadius(float64) {}
// SetImageURL sets the avatar's source URL and reloads it asynchronously.
func (a *Avatar) SetImageURL(url string) { func (a *Avatar) SetImageURL(url string) {
a.url = url a.url = url
a.Avatar.SetImageLoadFunc(a.loadFunc) a.refetch()
} }
// SetFromPixbuf sets the pixbuf. // SetFromPixbuf sets the pixbuf.
func (a *Avatar) SetFromPixbuf(pb *gdk.Pixbuf) { func (a *Avatar) SetFromPixbuf(pb *gdk.Pixbuf) {
a.cancelCtx()
a.pixbuf = pb a.pixbuf = pb
a.Avatar.SetImageLoadFunc(a.loadFunc) // a.Avatar.SetImageLoadFunc(a.loadFunc)
a.Avatar.QueueDraw()
} }
// SetFromAnimation sets the first frame of the animation.
func (a *Avatar) SetFromAnimation(pa *gdk.PixbufAnimation) { func (a *Avatar) SetFromAnimation(pa *gdk.PixbufAnimation) {
a.cancelCtx()
a.pixbuf = pa.GetStaticImage() a.pixbuf = pa.GetStaticImage()
a.Avatar.SetImageLoadFunc(a.loadFunc) // a.Avatar.SetImageLoadFunc(a.loadFunc)
a.Avatar.QueueDraw()
} }
// GetPixbuf returns the underlying pixbuf.
func (a *Avatar) GetPixbuf() *gdk.Pixbuf { func (a *Avatar) GetPixbuf() *gdk.Pixbuf {
return a.pixbuf return a.pixbuf
} }
@ -127,6 +138,7 @@ func (a *Avatar) GetImage() *gtk.Image {
return nil return nil
} }
// GetStorageType always returns IMAGE_PIXBUF.
func (a *Avatar) GetStorageType() gtk.ImageType { func (a *Avatar) GetStorageType() gtk.ImageType {
return gtk.IMAGE_PIXBUF return gtk.IMAGE_PIXBUF
} }

View File

@ -5,6 +5,8 @@ import (
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
// TODO: move roundimage.Button to rich.ImageButton.
// Button implements a rounded button with a rounded image. This widget only // Button implements a rounded button with a rounded image. This widget only
// supports a full circle for rounding. // supports a full circle for rounding.
type Button struct { type Button struct {
@ -20,7 +22,7 @@ var roundButtonCSS = primitives.PrepareClassCSS("round-button", `
`) `)
func NewButton() (*Button, error) { func NewButton() (*Button, error) {
image, _ := NewImage(0) image := NewImage(0)
image.Show() image.Show()
b := NewEmptyButton() b := NewEmptyButton()
@ -38,7 +40,7 @@ func NewEmptyButton() *Button {
} }
// NewCustomButton creates a new rounded button with the given Imager. If the // NewCustomButton creates a new rounded button with the given Imager. If the
// given Imager implements the Connector interface (aka *StaticImage), then the // given Imager implements the Connector interface (aka *StillImage), then the
// function will implicitly connect its handlers to the button. // function will implicitly connect its handlers to the button.
func NewCustomButton(img Imager) (*Button, error) { func NewCustomButton(img Imager) (*Button, error) {
b := NewEmptyButton() b := NewEmptyButton()

View File

@ -2,10 +2,12 @@ package roundimage
import ( import (
"context" "context"
"log"
"math" "math"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/handy"
"github.com/diamondburned/imgutil" "github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/cairo"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
@ -39,83 +41,201 @@ type Imager interface {
GetImage() *gtk.Image GetImage() *gtk.Image
} }
// Image represents an image with abstractions for asynchronously fetching
// images from a URL as well as having interchangeable fallbacks.
type Image struct { type Image struct {
*gtk.Image gtk.Image
Radius float64 Radius float64
style *gtk.StyleContext
procs []imgutil.Processor procs []imgutil.Processor
ifNone func(context.Context)
icon struct {
name string
size int
}
cancel context.CancelFunc
imgURL string
show bool
} }
var _ Imager = (*Image)(nil) var _ Imager = (*Image)(nil)
// NewImage creates a new round image. If radius is 0, then it will be half the // NewImage creates a new round image. If radius is 0, then it will be half the
// dimensions. If the radius is less than 0, then nothing is rounded. // dimensions. If the radius is less than 0, then nothing is rounded.
func NewImage(radius float64) (*Image, error) { func NewImage(radius float64) *Image {
i, err := gtk.ImageNew() i, err := gtk.ImageNew()
if err != nil { if err != nil {
return nil, err log.Panicln("failed to create new roundimage.Image:", err)
} }
image := &Image{Image: i, Radius: radius} style, _ := i.GetStyleContext()
image := &Image{
Image: *i,
Radius: radius,
style: style,
}
// Connect to the draw callback and clip the context. // Connect to the draw callback and clip the context.
i.Connect("draw", image.drawer) i.Connect("draw", image.drawer)
// Backup plan if Cairo's Surface is weird. return image
// var width, height int
// i.Connect("size-allocate", func(i *gtk.Image) {
// w := i.GetAllocatedWidth()
// h := i.GetAllocatedHeight()
// if width != w || height != h {
// log.Println("Image allocate", width, height, i.GetScaleFactor())
// width, height = w, h
// }
// })
return image, nil
} }
// NewSizedImage creates a new square image with the given square.
func NewSizedImage(radius float64, size int) *Image {
img := NewImage(radius)
img.SetSizeRequest(size, size)
return img
}
// AddProcessor adds image processors that will be processed on fetched images.
// Images generated internally, such as initials, won't use it.
func (i *Image) AddProcessor(procs ...imgutil.Processor) { func (i *Image) AddProcessor(procs ...imgutil.Processor) {
i.procs = append(i.procs, procs...) i.procs = append(i.procs, procs...)
} }
// GetImage returns the underlying image widget.
func (i *Image) GetImage() *gtk.Image { func (i *Image) GetImage() *gtk.Image {
return i.Image return &i.Image
} }
// Size returns the minimum side's length. This method is used when Image is
// supposed to be a square/circle.
func (i *Image) Size() int {
w, h := i.GetSizeRequest()
if w > h {
return h
}
return w
}
// SetSIze sets the iamge's physical size. It is a convenient function for
// SetSizeRequest.
func (i *Image) SetSize(size int) {
i.SetSizeRequest(size, size)
}
// SetIfNone sets the callback to be used if an empty URL is given to the image.
// If nil is given, then a fallback icon is used.
func (i *Image) SetIfNone(ifNone func(context.Context)) {
i.ifNone = ifNone
}
// UpdateIfNone updates the image if the image currently does not have one
// fetched from the URL. It does nothing otherwise.
func (i *Image) UpdateIfNone() {
if i.ifNone == nil || i.imgURL != "" {
return
}
i.SetImageURL("")
}
// SetPlaceholderIcon sets the placeholder icon onto the image. The given icon
// size does not affect the image's physical size.
func (i *Image) SetPlaceholderIcon(iconName string, iconPx int) {
i.icon.name = iconName
i.icon.size = iconPx
if i.imgURL == "" {
i.SetImageURL("")
}
}
// GetImageURL gets the image's URL. It returns an empty string if the image
// does not have a URL set.
func (i *Image) GetImageURL() string {
return i.imgURL
}
// SetImageURL sets the image's URL. If the URL is empty, then the placeholder
// icon is used, or the IfNone callback is called, or the pixbuf is cleared.
func (i *Image) SetImageURL(url string) { func (i *Image) SetImageURL(url string) {
// No dynamic sizing support; yolo. i.SetImageURLInto(url, i)
httputil.AsyncImage(context.Background(), i, url, i.procs...)
} }
// SetImageURLInto is SetImageURL, but the image container is given as an
// argument. It is used by other widgets that extend on this Image.
func (i *Image) SetImageURLInto(url string, otherImage httputil.ImageContainer) {
i.imgURL = url
// TODO: fix this context leak: cancel not being called on all paths.
ctx := i.resetCtx()
if url != "" {
// No dynamic sizing support; yolo.
httputil.AsyncImage(ctx, otherImage, url, i.procs...)
return
}
if i.icon.name != "" {
primitives.SetImageIcon(i, i.icon.name, i.icon.size)
goto noImage
}
if i.ifNone != nil {
i.ifNone(ctx)
return
}
noImage:
i.Image.SetFromPixbuf(nil)
i.cancel()
}
func (i *Image) resetCtx() context.Context {
if i.cancel != nil {
i.cancel()
i.cancel = nil
}
// TODO: fix this context leak: cancel not being called on all paths.
ctx, cancel := context.WithCancel(context.Background())
i.cancel = cancel
return ctx
}
// SetRadius sets the radius to be drawn with. If 0 is given, then a full circle
// is drawn, which only works best for images guaranteed to be square.
// Otherwise, the radius is either the number given or the minimum of either the
// width or height.
func (i *Image) SetRadius(r float64) { func (i *Image) SetRadius(r float64) {
i.Radius = r i.Radius = r
i.QueueDraw()
} }
func (i *Image) drawer(_ interface{}, cc *cairo.Context) bool { func (i *Image) drawer(image *gtk.Image, cc *cairo.Context) bool {
var w = float64(i.GetAllocatedWidth()) // Don't round if we're displaying a stock icon.
var h = float64(i.GetAllocatedHeight()) if i.imgURL == "" && i.icon.name != "" {
return false
}
var min = w a := image.GetAllocation()
// Use the smallest side for radius calculation. w := float64(a.GetWidth())
if h < w { h := float64(a.GetHeight())
min := w
// Use the largest side for radius calculation.
if h > w {
min = h min = h
} }
// Copy the variables in case we need to change them.
var r = i.Radius
switch { switch {
// If radius is less than 0, then don't round. // If radius is less than 0, then don't round.
case r < 0: case i.Radius < 0:
return false return false
// If radius is 0, then we have to calculate our own radius.:This only // If radius is 0, then we have to calculate our own radius.:This only
// works if the image is a square. // works if the image is a square.
case r == 0: case i.Radius == 0:
// Calculate the radius by dividing a side by 2. // Calculate the radius by dividing a side by 2.
r = (min / 2) r := (min / 2)
// Draw an arc from 0deg to 360deg. // Draw an arc from 0deg to 360deg.
cc.Arc(w/2, h/2, r, 0, circle) cc.Arc(w/2, h/2, r, 0, circle)
@ -129,10 +249,13 @@ func (i *Image) drawer(_ interface{}, cc *cairo.Context) bool {
// If radius is more than 0, then we have to calculate the radius from // If radius is more than 0, then we have to calculate the radius from
// the edges. // the edges.
case r > 0: case i.Radius > 0:
// StackOverflow is godly. // StackOverflow is godly.
// https://stackoverflow.com/a/6959843. // https://stackoverflow.com/a/6959843.
// Copy the variables so we can change them later.
r := i.Radius
// Radius should be largest a single side divided by 2. // Radius should be largest a single side divided by 2.
if max := min / 2; r > max { if max := min / 2; r > max {
r = max r = max
@ -157,3 +280,25 @@ func (i *Image) drawer(_ interface{}, cc *cairo.Context) bool {
return false return false
} }
// UseInitialsIfNone sets the given image to render an initial image if the
// image doesn't have a URL.
func (i *Image) UseInitialsIfNone(initialsFn func() string) {
i.SetIfNone(func(ctx context.Context) {
size := i.Size()
scale := i.GetScaleFactor()
a := handy.AvatarNew(size, initialsFn(), true)
p := a.DrawToPixbuf(size, scale)
if scale > 1 {
surface, _ := gdk.CairoSurfaceCreateFromPixbuf(p, scale, nil)
i.SetFromSurface(surface)
} else {
// Potentially save a copy.
i.SetFromPixbuf(p)
}
})
i.UpdateIfNone()
}

View File

@ -1,49 +1,44 @@
package roundimage package roundimage
import ( import (
"context"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/cairo"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
) )
// StaticImage is an image that only plays a GIF if it's hovered on top of. // StillImage is an image that only plays a GIF if it's hovered on top of.
type StaticImage struct { type StillImage struct {
*Image *Image
animating bool animating bool
animation *gdk.PixbufAnimation animation *gdk.PixbufAnimation
} }
var ( var (
_ Imager = (*StaticImage)(nil) _ Imager = (*StillImage)(nil)
_ Connector = (*StaticImage)(nil) _ Connector = (*StillImage)(nil)
_ httputil.ImageContainer = (*StaticImage)(nil) _ httputil.ImageContainer = (*StillImage)(nil)
) )
func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage, error) { // NewStillImage creates a new static that binds to the parent's handler so
i, err := NewImage(radius) // that the image only animates when parent is hovered over.
if err != nil { func NewStillImage(parent primitives.Connector, radius float64) *StillImage {
return nil, err i := NewImage(radius)
}
var s = &StaticImage{i, false, nil} s := StillImage{i, false, nil}
if parent != nil { s.ConnectHandlers(parent)
s.ConnectHandlers(parent)
}
return s, nil return &s
} }
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) { func (s *StillImage) ConnectHandlers(connector primitives.Connector) {
connector.Connect("enter-notify-event", func(interface{}) { connector.Connect("enter-notify-event", func() {
if s.animation != nil && !s.animating { if s.animation != nil && !s.animating {
s.animating = true s.animating = true
s.Image.SetFromAnimation(s.animation) s.Image.SetFromAnimation(s.animation)
} }
}) })
connector.Connect("leave-notify-event", func(interface{}) { connector.Connect("leave-notify-event", func() {
if s.animation != nil && s.animating { if s.animation != nil && s.animating {
s.animating = false s.animating = false
s.Image.SetFromPixbuf(s.animation.GetStaticImage()) s.Image.SetFromPixbuf(s.animation.GetStaticImage())
@ -51,26 +46,26 @@ func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
}) })
} }
func (s *StaticImage) SetImageURL(url string) { // SetImageURL sets the image's URL.
// No dynamic sizing support; yolo. func (s *StillImage) SetImageURL(url string) {
httputil.AsyncImage(context.Background(), s, url, s.Image.procs...) s.Image.SetImageURLInto(url, s)
} }
func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) { func (s *StillImage) SetFromPixbuf(pb *gdk.Pixbuf) {
s.animation = nil s.animation = nil
s.Image.SetFromPixbuf(pb) s.Image.SetFromPixbuf(pb)
} }
func (s *StaticImage) SetFromSurface(sf *cairo.Surface) { func (s *StillImage) SetFromSurface(sf *cairo.Surface) {
s.animation = nil s.animation = nil
s.Image.SetFromSurface(sf) s.Image.SetFromSurface(sf)
} }
func (s *StaticImage) SetFromAnimation(anim *gdk.PixbufAnimation) { func (s *StillImage) SetFromAnimation(anim *gdk.PixbufAnimation) {
s.animation = anim s.animation = anim
s.Image.SetFromPixbuf(anim.GetStaticImage()) s.Image.SetFromPixbuf(anim.GetStaticImage())
} }
func (s *StaticImage) GetAnimation() *gdk.PixbufAnimation { func (s *StillImage) GetAnimation() *gdk.PixbufAnimation {
return s.animation return s.animation
} }

View File

@ -1,219 +1,35 @@
package rich package rich
import ( import "log"
"context"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
"github.com/diamondburned/cchat/text"
"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
primitives.ImageIconSetter
roundimage.RadiusSetter
// ImageURLSetter describes an image that can be set a URL.
type ImageURLSetter interface {
SetImageURL(url string) SetImageURL(url string)
GetStorageType() gtk.ImageType
GetPixbuf() *gdk.Pixbuf
GetAnimation() *gdk.PixbufAnimation
} }
var ( type tooltipSetter interface {
_ RoundIconContainer = (*roundimage.Image)(nil) SetTooltipText(text string)
_ RoundIconContainer = (*roundimage.StaticImage)(nil)
)
// Icon represents a rounded image container.
type Icon struct {
*gtk.Revealer // TODO move out
Image RoundIconContainer
// state
url string
} }
const DefaultIconSize = 16 // BindRoundImage binds a round image to a rich label state store.
func BindRoundImage(img ImageURLSetter, state LabelStateStorer, tooltip bool) {
var setTooltip func(string)
var _ cchat.IconContainer = (*Icon)(nil) if tooltip {
tooltipper, ok := img.(tooltipSetter)
func NewIcon(sizepx int) *Icon { if !ok {
img, _ := roundimage.NewImage(0) log.Panicf("img of type %T is not tooltipSetter", img)
img.Show()
return NewCustomIcon(img, sizepx)
}
func NewCustomIcon(img RoundIconContainer, sizepx int) *Icon {
if sizepx == 0 {
sizepx = DefaultIconSize
}
rev, _ := gtk.RevealerNew()
rev.Add(img)
rev.SetRevealChild(false)
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
rev.SetTransitionDuration(50)
i := &Icon{
Revealer: rev,
Image: img,
}
i.SetSize(sizepx)
return i
}
// URL is not thread-safe.
func (i *Icon) URL() string {
return i.url
}
func (i *Icon) CopyPixbuf(dst httputil.ImageContainer) {
switch i.Image.GetStorageType() {
case gtk.IMAGE_PIXBUF:
dst.SetFromPixbuf(i.Image.GetPixbuf())
case gtk.IMAGE_ANIMATION:
dst.SetFromAnimation(i.Image.GetAnimation())
}
}
// Thread-unsafe setter methods should only be called right after construction.
// SetPlaceholderIcon is not thread-safe.
func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
i.Image.SetRadius(-1) // square
i.SetRevealChild(true)
if iconName != "" {
primitives.SetImageIcon(i.Image, iconName, iconSzPx)
}
}
// SetSize is not thread-safe.
func (i *Icon) SetSize(szpx int) {
i.Image.SetSizeRequest(szpx, szpx)
}
// Size returns the minimum of the image size. It is not thread-safe.
func (i *Icon) Size() int {
w, h := i.Image.GetSizeRequest()
if h < w {
return h
}
return w
}
// SetIcon is thread-safe.
func (i *Icon) SetIcon(url string) {
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
}
func (i *Icon) AsyncSetIconer(iconer cchat.Iconer, errwrap string) {
// Reveal to show the placeholder.
i.SetRevealChild(true)
// I have a hunch this will never work; as long as Go keeps a reference with
// iconer.Icon, then destroy will never be triggered.
ctx := primitives.HandleDestroyCtx(context.Background(), i)
gts.Async(func() (func(), error) {
f, err := iconer.Icon(ctx, i)
if err != nil {
return nil, errors.Wrap(err, "failed to load iconer")
} }
return func() { i.Connect("destroy", func(interface{}) { f() }) }, nil setTooltip = tooltipper.SetTooltipText
}
state.OnUpdate(func() {
image := state.Image()
img.SetImageURL(image.URL)
if setTooltip != nil {
setTooltip(state.Label().String())
}
}) })
} }
// SetIconUnsafe is not thread-safe.
func (i *Icon) SetIconUnsafe(url string) {
// Setting the radius here since we resetted it for a placeholder icon.
i.Image.SetRadius(0)
i.SetRevealChild(true)
i.url = url
i.Image.SetImageURL(i.url)
}
// type EventIcon struct {
// *gtk.EventBox
// Icon *Icon
// }
// func NewEventIcon(sizepx int) *EventIcon {
// icn := NewIcon(sizepx)
// return WrapEventIcon(icn)
// }
// func WrapEventIcon(icn *Icon) *EventIcon {
// icn.Show()
// evb, _ := gtk.EventBoxNew()
// evb.Add(icn)
// return &EventIcon{
// EventBox: evb,
// Icon: icn,
// }
// }
type ToggleButtonImage struct {
gtk.ToggleButton
Labeler
cchat.IconContainer
Label *gtk.Label
Image *Icon
Box *gtk.Box
}
var (
_ gtk.IWidget = (*ToggleButton)(nil)
_ cchat.LabelContainer = (*ToggleButton)(nil)
)
func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
img, _ := roundimage.NewStaticImage(nil, 0)
img.Show()
return NewCustomToggleButtonImage(img, content)
}
func NewCustomToggleButtonImage(img RoundIconContainer, content text.Rich) *ToggleButtonImage {
l := NewLabel(content)
l.Show()
i := NewCustomIcon(img, 0)
i.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(i, false, false, 0)
box.PackStart(l, true, true, 5)
box.Show()
b, _ := gtk.ToggleButtonNew()
b.Add(box)
if connector, ok := img.(roundimage.Connector); ok {
connector.ConnectHandlers(b)
}
return &ToggleButtonImage{
ToggleButton: *b,
Labeler: l, // easy inheritance of methods
IconContainer: i,
Label: &l.Label,
Image: i,
Box: box,
}
}

View File

@ -1,134 +1,94 @@
package rich package rich
import ( import (
"context"
"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/rich/parser/markup" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango" "github.com/gotk3/gotk3/pango"
"github.com/pkg/errors"
) )
type Labeler interface { // LabelRenderer is the input/output function to render a rich text segment to
// thread-safe // Pango markup.
cchat.LabelContainer // thread-safe type LabelRenderer = func(text.Rich) markup.RenderOutput
// not thread-safe // RenderSkipImages is a label renderer that skips images.
SetLabelUnsafe(text.Rich) func RenderSkipImages(rich text.Rich) markup.RenderOutput {
GetLabel() text.Rich return markup.RenderCmplxWithConfig(rich, markup.RenderConfig{
GetText() string SkipImages: true,
} NoMentionLinks: true,
// SuperLabeler represents a label that inherits the current labeler.
type SuperLabeler interface {
SetLabelUnsafe(text.Rich)
}
type LabelerFn = func(context.Context, cchat.LabelContainer) (func(), error)
type Label struct {
gtk.Label
Current text.Rich
// super unexported field for inheritance
super SuperLabeler
}
var (
_ gtk.IWidget = (*Label)(nil)
_ Labeler = (*Label)(nil)
)
func NewLabel(content text.Rich) *Label {
label, _ := gtk.LabelNew("")
label.SetMarkup(markup.Render(content))
label.SetXAlign(0) // left align
label.SetEllipsize(pango.ELLIPSIZE_END)
l := &Label{
Label: *label,
Current: content,
}
return l
}
// NewInheritLabel creates a new label wrapper for structs that inherit this
// label.
func NewInheritLabel(super SuperLabeler) *Label {
l := NewLabel(text.Rich{})
l.super = super
return l
}
func (l *Label) validsuper() bool {
_, ok := l.super.(*Label)
// supers must not be the current struct and must not be nil.
return !ok && l.super != nil
}
func (l *Label) AsyncSetLabel(fn LabelerFn, info string) {
ctx := primitives.HandleDestroyCtx(context.Background(), l)
gts.Async(func() (func(), error) {
f, err := fn(ctx, l)
if err != nil {
return nil, errors.Wrap(err, "failed to load iconer")
}
return func() { l.Connect("destroy", func(interface{}) { f() }) }, nil
}) })
} }
// SetLabel is thread-safe. // Label provides an abstraction around a regular GTK label that can be
func (l *Label) SetLabel(content text.Rich) { // self-updated. Widgets that extend off of this (such as ToggleButton) does not
gts.ExecAsync(func() { l.SetLabelUnsafe(content) }) // need to manually
type Label struct {
gtk.Label
label text.Rich
output markup.RenderOutput
render LabelRenderer
} }
// SetLabelUnsafe sets the label in the current thread, meaning it's not var _ gtk.IWidget = (*Label)(nil)
// thread-safe. If this label has a super, then it will call that struct's
// SetLabelUnsafe instead of its own.
func (l *Label) SetLabelUnsafe(content text.Rich) {
l.Current = content
if l.validsuper() { // NewStaticLabel creates a static, non-updating label.
l.super.SetLabelUnsafe(content) func NewStaticLabel(rich text.Rich) *Label {
} else { label, _ := gtk.LabelNew("")
l.SetMarkup(markup.Render(content)) label.SetXAlign(0) // left align
label.SetEllipsize(pango.ELLIPSIZE_END)
if !rich.IsEmpty() {
label.SetMarkup(markup.Render(rich))
} }
return &Label{Label: *label}
} }
// GetLabel is NOT thread-safe. // NewLabel creates a self-updating label.
func (l *Label) GetLabel() text.Rich { func NewLabel(state LabelStateStorer) *Label {
return l.Current return NewLabelWithRenderer(state, nil)
} }
// GetText is NOT thread-safe. // NewLabelWithRenderer creates a self-updating label using the given renderer.
func (l *Label) GetText() string { func NewLabelWithRenderer(state LabelStateStorer, r LabelRenderer) *Label {
return l.Current.Content l := NewStaticLabel(text.Plain(""))
l.render = r
state.OnUpdate(func() { l.SetLabel(state.Label()) })
return l
} }
type ToggleButton struct { // Output returns the rendered output.
gtk.ToggleButton func (l *Label) Output() markup.RenderOutput {
Label return l.output
} }
var ( // SetLabel sets the label in the current thread, meaning it's not thread-safe.
_ gtk.IWidget = (*ToggleButton)(nil) func (l *Label) SetLabel(content text.Rich) {
_ cchat.LabelContainer = (*ToggleButton)(nil) // Save a call if the content is empty.
) if content.IsEmpty() {
l.label = content
l.output = markup.RenderOutput{}
func NewToggleButton(content text.Rich) *ToggleButton { return
l := NewLabel(content) }
l.Show()
b, _ := gtk.ToggleButtonNew() l.label = content
primitives.BinLeftAlignLabel(b)
b.Add(l) var out markup.RenderOutput
if l.render != nil {
out = l.render(content)
} else {
out = markup.RenderCmplx(content)
}
return &ToggleButton{*b, *l} l.output = out
l.SetMarkup(out.Markup)
l.SetTooltipMarkup(out.Markup)
}
// SetRenderer sets a custom renderer. If the given renderer is nil, then the
// default markup renderer is used instead. The label is automatically updated.
func (l *Label) SetRenderer(renderer LabelRenderer) {
l.render = renderer
l.SetLabel(l.label)
} }

View File

@ -31,69 +31,35 @@ const (
MaxHeight = 500 MaxHeight = 500
) )
type WidgetConnector interface { // // Labeler implements a rich label that stores an output state.
gtk.IWidget // type Labeler interface {
primitives.Connector // WidgetConnector
} // rich.Labeler
// Output() markup.RenderOutput
var _ WidgetConnector = (*gtk.Label)(nil) // }
// Labeler implements a rich label that stores an output state.
type Labeler interface {
WidgetConnector
rich.Labeler
Output() markup.RenderOutput
}
// Label implements a label that's already bounded to the markup URI handlers. // Label implements a label that's already bounded to the markup URI handlers.
type Label struct { type Label struct {
*rich.Label *rich.Label
*BoundBox *BoundBox
output markup.RenderOutput
} }
var ( // var (
_ Labeler = (*Label)(nil) // _ Labeler = (*Label)(nil)
_ rich.SuperLabeler = (*Label)(nil) // _ rich.SuperLabeler = (*Label)(nil)
) // )
func NewLabel(txt text.Rich) *Label { func NewLabel(txt text.Rich) *Label {
l := &Label{} l := &Label{}
l.Label = rich.NewInheritLabel(l) l.Label = rich.NewStaticLabel(txt)
l.Label.SetLabelUnsafe(txt) // test
// Bind and return. // Bind and return.
l.BoundBox = BindRichLabel(l) l.BoundBox = BindRichLabel(l.Label)
return l return l
} }
func (l *Label) Reset() { func (l *Label) Reset() {
l.output = markup.RenderOutput{} l.Label.SetLabel(text.Plain(""))
}
func (l *Label) SetLabelUnsafe(content text.Rich) {
l.output = markup.RenderCmplx(content)
l.SetMarkup(l.output.Markup)
}
// Output returns the label's markup output. This function is NOT
// thread-safe.
func (l *Label) Output() markup.RenderOutput {
return l.output
}
// SetOutput sets the internal output and label. It preserves the tail if
// any.
func (l *Label) SetOutput(o markup.RenderOutput) {
l.output = o
l.SetMarkup(o.Markup)
}
// SetUnderlyingOutput sets the output state without changing the label's
// markup. This is useful for internal use cases where the label is updated
// separately.
func (l *Label) SetUnderlyingOutput(o markup.RenderOutput) {
l.output = o
} }
type ReferenceHighlighter interface { type ReferenceHighlighter interface {
@ -103,11 +69,11 @@ type ReferenceHighlighter interface {
// BoundBox is a box wrapping elements that can be interacted with from the // BoundBox is a box wrapping elements that can be interacted with from the
// parsed labels. // parsed labels.
type BoundBox struct { type BoundBox struct {
label Labeler label *rich.Label
refer ReferenceHighlighter refer ReferenceHighlighter
} }
func BindRichLabel(label Labeler) *BoundBox { func BindRichLabel(label *rich.Label) *BoundBox {
bound := BoundBox{label: label} bound := BoundBox{label: label}
bind(label, bound.activate) bind(label, bound.activate)
return &bound return &bound
@ -206,7 +172,7 @@ func NewPopoverMentioner(rel gtk.IWidget, input string, segment text.Segment) *g
l.Show() l.Show()
// Enable images??? // Enable images???
BindActivator(l) BindImagePreview(l)
// Make a scrolling text. // Make a scrolling text.
scr := scrollinput.NewVScroll(PopoverWidth) scr := scrollinput.NewVScroll(PopoverWidth)
@ -259,13 +225,23 @@ func popoverImg(url string, round bool) gtk.IWidget {
return btn return btn
} }
func BindActivator(connector WidgetConnector) { // WidgetConnector describes a connector that is also a widget.
type WidgetConnector interface {
gtk.IWidget
primitives.Connector
}
var _ WidgetConnector = (*gtk.Label)(nil)
// BindImagePreview binds activate-link to the label callback so that images
// have a popover preview.
func BindImagePreview(connector WidgetConnector) {
bind(connector, nil) bind(connector, nil)
} }
// bind connects activate-link. If activator returns true, then nothing is done. // bind connects activate-link. If activator returns true, then nothing is done.
// Activator can be nil. // Activator can be nil.
func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle) bool) { func bind(c WidgetConnector, activator func(uri string, r gdk.Rectangle) bool) {
// This implementation doesn't seem like a good idea. First off, is the // This implementation doesn't seem like a good idea. First off, is the
// closure really garbage collected? If it's not, then we have some huge // closure really garbage collected? If it's not, then we have some huge
// issues. Second, if the closure is garbage collected, then when? If it's // issues. Second, if the closure is garbage collected, then when? If it's
@ -273,11 +249,11 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle)
// message, but we're also keeping alive the widget. // message, but we're also keeping alive the widget.
var x, y float64 var x, y float64
connector.Connect("motion-notify-event", func(_ interface{}, ev *gdk.Event) { c.Connect("motion-notify-event", func(_ interface{}, ev *gdk.Event) {
x, y = gdk.EventMotionNewFromEvent(ev).MotionVal() x, y = gdk.EventMotionNewFromEvent(ev).MotionVal()
}) })
connector.Connect("activate-link", func(c WidgetConnector, uri string) bool { c.Connect("activate-link", func(c WidgetConnector, uri string) bool {
// Make a new rectangle to use in the popover. // Make a new rectangle to use in the popover.
r := gdk.Rectangle{} r := gdk.Rectangle{}
r.SetX(int(x)) r.SetX(int(x))
@ -297,8 +273,8 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle)
if !round { if !round {
img, _ = gtk.ImageNew() img, _ = gtk.ImageNew()
} else { } else {
r, _ := roundimage.NewImage(0) r := roundimage.NewImage(0)
img = r.Image img = &r.Image
} }
img.SetSizeRequest(w, h) img.SetSizeRequest(w, h)

View File

@ -123,6 +123,10 @@ type RenderConfig struct {
// mentions. // mentions.
NoReferencing bool NoReferencing bool
// SkipImages skips rendering any image markup. This is useful for widgets
// that already render an outside image.
SkipImages bool
// AnchorColor forces all anchors to be of a certain color. This is used if // AnchorColor forces all anchors to be of a certain color. This is used if
// the boolean is true. Else, all mention links will not work and regular // the boolean is true. Else, all mention links will not work and regular
// links will be of the default color. // links will be of the default color.
@ -197,7 +201,7 @@ func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
} }
// Only inline images if start == end per specification. // Only inline images if start == end per specification.
if start == end { if !cfg.SkipImages && start == end {
if imager := segment.AsImager(); imager != nil { if imager := segment.AsImager(); imager != nil {
appended.Open(start, composeImageMarkup(imager)) appended.Open(start, composeImageMarkup(imager))
} }

View File

@ -1,22 +1,236 @@
package rich package rich
import ( import (
"context"
"html" "html"
"image"
"runtime"
"sync"
"github.com/diamondburned/cchat"
"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/rich/parser/markup"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/pkg/errors"
) )
func Small(text string) string { // Small is a renderer that makes the plain text small.
return `<span size="small" color="#808080">` + text + "</span>" func Small(content text.Rich) markup.RenderOutput {
v := `<span size="small" alpha="50%">` + html.EscapeString(content.Content) + "</span>"
return markup.RenderOutput{
Markup: v,
Input: content.Content,
}
} }
func MakeRed(content text.Rich) string { // MakeRed is a renderer that makes the plain text red.
return `<span color="red">` + html.EscapeString(content.Content) + `</span>` func MakeRed(content text.Rich) markup.RenderOutput {
v := `<span color="red">` + html.EscapeString(content.Content) + `</span>`
return markup.RenderOutput{
Markup: v,
Input: content.Content,
}
} }
// used for grabbing text without changing state // LabelStateStorer is an interface for LabelState.
type nullLabel struct { type LabelStateStorer interface {
text.Rich Label() text.Rich
Image() LabelImage
OnUpdate(func()) (remove func())
} }
func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t } var _ LabelStateStorer = (*LabelState)(nil)
// NameContainer contains a reusable LabelState for cchat.Namer.
type NameContainer struct {
LabelState
state *containerState // for alignment
}
type containerState struct {
// stop is the stored callback.
stop func()
// current is the context stopper being used.
current context.CancelFunc
}
// Stop stops the name container. Calling Stop twice when no new namers are set
// will do nothing.
func (namec *NameContainer) Stop() {
if namec.state != nil {
namec.state.Stop()
namec.LabelState.setLabel(text.Plain(""))
}
}
func (state *containerState) Stop() {
if state.current != nil {
state.current()
state.current = nil
}
if state.stop != nil {
state.stop()
state.stop = nil
}
}
// QueueNamer tries using the namer in the background and queue the setter onto
// the next GLib loop iteration.
func (namec *NameContainer) QueueNamer(ctx context.Context, namer cchat.Namer) {
if namec.state == nil {
namec.state = &containerState{}
runtime.SetFinalizer(namec.state, (*containerState).Stop)
}
namec.Stop()
ctx, cancel := context.WithCancel(ctx)
namec.state.current = cancel
go func() {
stop, err := namer.Name(ctx, namec)
if err != nil {
log.Error(errors.Wrap(err, "failed to activate namer"))
}
gts.ExecAsync(func() {
namec.state.current()
namec.state.current = nil
namec.state.stop = stop
})
}()
}
// BindNamer binds a destructor signal to the name container to cancel a
// context.
func (namec *NameContainer) BindNamer(w primitives.Connector, sig string, namer cchat.Namer) {
namec.QueueNamer(context.Background(), namer)
// TODO: I have a hunch that everything below this will leak to hell. Just a
// hunch.
// namec.Stop()
// ctx, cancel := context.WithCancel(context.Background())
// namec.current = cancel
// // TODO: this might leak, because namec.Stop references the fns list which
// // might reference w indirectly.
// w.Connect(sig, namec.Stop)
// go func() {
// stop, err := namer.Name(ctx, namec)
// if err != nil {
// log.Error(errors.Wrap(err, "failed to activate namer"))
// }
// gts.ExecAsync(func() {
// namec.current()
// namec.current = nil
// namec.stop = stop // nil is OK.
// })
// }()
}
// LabelState provides a container for labels that allow other containers to
// extend upon. A zero-value instance is a valid instance.
type LabelState struct {
// don't copy LabelState.
_ [0]sync.Mutex
label text.Rich
fns map[int]func()
serial int
}
var _ cchat.LabelContainer = (*LabelState)(nil)
// NewLabelState creates a new label state.
func NewLabelState(l text.Rich) *LabelState {
return &LabelState{label: l}
}
// String returns the inside label in plain text.
func (state *LabelState) String() string {
return state.label.Content
}
// Label returns the inside label.
func (state *LabelState) Label() text.Rich {
return state.label
}
// OnUpdate subscribes the given callback. The returned callback removes the
// given callback from the registry.
func (state *LabelState) OnUpdate(fn func()) (remove func()) {
if state.fns == nil {
state.fns = make(map[int]func(), 1)
}
id := state.serial
state.fns[id] = fn
state.serial++
if !state.label.IsEmpty() {
fn()
}
return func() { delete(state.fns, id) }
}
// SetLabel is called by cchat to update the state. The internal subscribed
// callbacks will be called in the main thread.
func (state *LabelState) SetLabel(label text.Rich) {
gts.ExecAsync(func() { state.setLabel(label) })
}
func (state *LabelState) setLabel(label text.Rich) {
state.label = label
for _, fn := range state.fns {
fn()
}
}
// LabelImage is the first image from a label. If
type LabelImage struct {
URL string
Text string
Size image.Point
Avatar bool
}
// HasImage returns true if the label has an image.
func (labelImage LabelImage) HasImage() bool {
return labelImage.URL != ""
}
// Image returns the image, if any. Otherwise, an empty string is returned.
func (state *LabelState) Image() LabelImage {
for _, segment := range state.label.Segments {
if imager := segment.AsImager(); imager != nil {
return LabelImage{
URL: imager.Image(),
Text: imager.ImageText(),
Size: image.Pt(imager.ImageSize()),
}
}
if avatarer := segment.AsAvatarer(); avatarer != nil {
size := avatarer.AvatarSize()
return LabelImage{
URL: avatarer.Avatar(),
Text: avatarer.AvatarText(),
Size: image.Pt(size, size),
Avatar: true,
}
}
}
return LabelImage{}
}

View File

@ -4,15 +4,12 @@ package config
import ( import (
"fmt" "fmt"
"hash/fnv"
"io"
"strconv"
"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/ui/config" "github.com/diamondburned/cchat-gtk/internal/ui/config"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -31,17 +28,17 @@ func Restore(conf Configurator) {
gts.Async(func() (func(), error) { gts.Async(func() (func(), error) {
c, err := conf.Configuration() c, err := conf.Configuration()
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to get %s config", conf.Name()) return nil, errors.Wrapf(err, "failed to get %s config", conf.ID())
} }
file := serviceFile(conf) file := serviceFile(conf)
if err := config.UnmarshalFromFile(file, &c); err != nil { if err := config.UnmarshalFromFile(file, &c); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal %s config", conf.Name()) return nil, errors.Wrapf(err, "failed to unmarshal %s config", conf.ID())
} }
if err := conf.SetConfiguration(c); err != nil { if err := conf.SetConfiguration(c); err != nil {
return nil, errors.Wrapf(err, "failed to set %s config", conf.Name()) return nil, errors.Wrapf(err, "failed to set %s config", conf.ID())
} }
return nil, nil return nil, nil
@ -52,16 +49,16 @@ func Spawn(conf Configurator) error {
gts.Async(func() (func(), error) { gts.Async(func() (func(), error) {
c, err := conf.Configuration() c, err := conf.Configuration()
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to get %s config", conf.Name()) return nil, errors.Wrapf(err, "failed to get %s config", conf.ID())
} }
file := serviceFile(conf) file := serviceFile(conf)
err = config.UnmarshalFromFile(file, &c) err = config.UnmarshalFromFile(file, &c)
err = errors.Wrapf(err, "failed to unmarshal %s config", conf.Name()) err = errors.Wrapf(err, "failed to unmarshal %s config", conf.ID())
return func() { return func() {
spawn(conf.Name().String(), c, func(finalized bool) error { spawn(conf, c, func(finalized bool) error {
if err := conf.SetConfiguration(c); err != nil { if err := conf.SetConfiguration(c); err != nil {
return err return err
} }
@ -81,16 +78,10 @@ func Spawn(conf Configurator) error {
} }
func serviceFile(conf Configurator) string { func serviceFile(conf Configurator) string {
return fmt.Sprintf("service-%s.json", dumbHash(conf.Name())) return fmt.Sprintf("services/%s.json", conf.ID())
} }
func dumbHash(name text.Rich) string { func spawn(c Configurator, conf map[string]string, apply func(final bool) error) {
hash := fnv.New32a()
io.WriteString(hash, name.String())
return strconv.FormatUint(uint64(hash.Sum32()), 36)
}
func spawn(name string, conf map[string]string, apply func(final bool) error) {
container := newContainer(conf, func() error { return apply(false) }) container := newContainer(conf, func() error { return apply(false) })
container.Grid.SetVAlign(gtk.ALIGN_START) container.Grid.SetVAlign(gtk.ALIGN_START)
@ -107,20 +98,25 @@ func spawn(name string, conf map[string]string, apply func(final bool) error) {
b.PackStart(container.ErrHeader, false, false, 0) b.PackStart(container.ErrHeader, false, false, 0)
b.Show() b.Show()
var title = "Configure " + name
h, _ := gtk.HeaderBarNew() h, _ := gtk.HeaderBarNew()
h.SetTitle(title) h.SetTitle("Configure " + c.ID())
h.SetShowCloseButton(true) h.SetShowCloseButton(true)
h.Show() h.Show()
var state rich.NameContainer
state.OnUpdate(func() {
h.SetTitle("Configure " + state.String())
})
d, _ := gts.NewEmptyModalDialog() d, _ := gts.NewEmptyModalDialog()
d.SetDefaultSize(400, 300) d.SetDefaultSize(400, 300)
d.Add(b) d.Add(b)
d.SetTitle(title)
d.SetTitlebar(h) d.SetTitlebar(h)
d.Connect("destroy", func(*gtk.Dialog) { apply(true) }) // Bind the title.
state.BindNamer(d, "response", c)
// Bind the updater.
d.Connect("response", func(*gtk.Dialog) { apply(true) })
d.Show() d.Show()
} }

View File

@ -131,7 +131,10 @@ type sizeBinder interface {
var _ sizeBinder = (*List)(nil) var _ sizeBinder = (*List)(nil)
func (h *Header) AppMenuBindSize(c sizeBinder) { func (h *Header) AppMenuBindSize(c sizeBinder) {
c.Connect("size-allocate", func(c sizeBinder) { update := func(c sizeBinder) {
h.AppMenu.SetSizeRequest(c.GetAllocatedWidth(), -1) h.AppMenu.SetSizeRequest(c.GetAllocatedWidth(), -1)
}) }
c.Connect("size-allocate", update)
update(c)
} }

View File

@ -35,7 +35,8 @@ var _ ListController = (*List)(nil)
var listCSS = primitives.PrepareClassCSS("service-list", ` var listCSS = primitives.PrepareClassCSS("service-list", `
.service-list { .service-list {
padding: 0; padding: 0;
background-color: mix(@theme_bg_color, @theme_fg_color, 0.03); background-color: @theme_bg_color;
/* background-color: mix(@theme_bg_color, @theme_fg_color, 0.03); */
} }
`) `)

View File

@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"fmt" "fmt"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
@ -11,7 +12,6 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag"
"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/rich/parser/markup"
"github.com/diamondburned/cchat-gtk/internal/ui/service/config" "github.com/diamondburned/cchat-gtk/internal/ui/service/config"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session" "github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server" "github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
@ -19,7 +19,7 @@ import (
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
const IconSize = 48 const IconSize = 42
type ListController interface { type ListController interface {
// ClearMessenger is called when a nil slice of servers is set. // ClearMessenger is called when a nil slice of servers is set.
@ -43,13 +43,14 @@ type Service struct {
*gtk.Box *gtk.Box
Button *gtk.ToggleButton Button *gtk.ToggleButton
Icon *rich.Icon Icon *roundimage.StillImage
Menu *actions.Menu Menu *actions.Menu
BodyRev *gtk.Revealer // revealed BodyRev *gtk.Revealer // revealed
BodyList *session.List // not really supposed to be here BodyList *session.List // not really supposed to be here
service cchat.Service // state service cchat.Service // state
Name rich.NameContainer
Configurator cchat.Configurator Configurator cchat.Configurator
} }
@ -67,7 +68,7 @@ var serviceCSS = primitives.PrepareClassCSS("service", `
var serviceButtonCSS = primitives.PrepareClassCSS("service-button", ` var serviceButtonCSS = primitives.PrepareClassCSS("service-button", `
.service-button { .service-button {
padding: 2px; padding: 6px;
margin: 0; margin: 0;
} }
@ -78,58 +79,51 @@ var serviceButtonCSS = primitives.PrepareClassCSS("service-button", `
.service-button:checked { .service-button:checked {
border-radius: 14px 14px 0 0; border-radius: 14px 14px 0 0;
background-color: alpha(@theme_fg_color, 0.2); /* background-color: alpha(@theme_fg_color, 0.2); */
} }
`) `)
var serviceIconCSS = primitives.PrepareClassCSS("service-icon", ` var serviceIconCSS = primitives.PrepareClassCSS("service-icon", ``)
.service-icon { padding: 4px }
`)
func NewService(svc cchat.Service, svclctrl ListController) *Service { func NewService(svc cchat.Service, svclctrl ListController) *Service {
service := &Service{ service := Service{
service: svc, service: svc,
ListController: svclctrl, ListController: svclctrl,
} }
service.Name.QueueNamer(context.Background(), svc)
service.BodyList = session.NewList(service) service.BodyList = session.NewList(&service)
service.BodyList.Show() service.BodyList.Show()
service.BodyRev, _ = gtk.RevealerNew() service.BodyRev, _ = gtk.RevealerNew()
service.BodyRev.SetRevealChild(false) // TODO persistent state service.BodyRev.SetRevealChild(false) // TODO persistent state
service.BodyRev.SetTransitionDuration(50) service.BodyRev.SetTransitionDuration(100)
service.BodyRev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_DOWN) service.BodyRev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_DOWN)
service.BodyRev.Add(service.BodyList) service.BodyRev.Add(service.BodyList)
service.BodyRev.Show() service.BodyRev.Show()
// TODO: have it so the button changes to the session avatar when collapsed // TODO: have it so the button changes to the session avatar when collapsed
avatar := roundimage.NewAvatar(IconSize)
avatar.SetText(svc.Name().String())
avatar.Show()
service.Icon = rich.NewCustomIcon(avatar, IconSize)
service.Icon.Show()
// potentially nonstandard
service.Icon.SetPlaceholderIcon("text-html-symbolic", IconSize)
// TODO: hover for name. We use tooltip for now.
service.Icon.SetTooltipMarkup(markup.Render(svc.Name()))
serviceIconCSS(service.Icon)
if iconer := svc.AsIconer(); iconer != nil {
service.Icon.AsyncSetIconer(iconer, "Failed to set service icon")
}
service.Button, _ = gtk.ToggleButtonNew() service.Button, _ = gtk.ToggleButtonNew()
service.Button.Add(service.Icon)
service.Button.SetRelief(gtk.RELIEF_NONE) service.Button.SetRelief(gtk.RELIEF_NONE)
service.Button.Show() service.Button.Show()
serviceButtonCSS(service.Button)
service.Icon = roundimage.NewStillImage(service.Button, 0)
service.Icon.SetSize(IconSize)
service.Icon.UseInitialsIfNone(service.Name.String)
service.Icon.Show()
serviceIconCSS(service.Icon)
rich.BindRoundImage(service.Icon, &service.Name, true)
// TODO: hover for name. We use tooltip for now.
service.Button.Add(service.Icon)
service.Button.Connect("clicked", func(tb *gtk.ToggleButton) { service.Button.Connect("clicked", func(tb *gtk.ToggleButton) {
revealed := !service.GetRevealChild() revealed := !service.GetRevealChild()
service.SetRevealChild(revealed) service.SetRevealChild(revealed)
tb.SetActive(revealed) tb.SetActive(revealed)
}) })
serviceButtonCSS(service.Button)
// Bind session.* actions into row. // Bind session.* actions into row.
service.Menu = actions.NewMenu("service") service.Menu = actions.NewMenu("service")
@ -153,9 +147,9 @@ func NewService(svc cchat.Service, svclctrl ListController) *Service {
serviceCSS(service.Box) serviceCSS(service.Box)
// Bind a drag and drop on the button instead of the entire box. // Bind a drag and drop on the button instead of the entire box.
drag.BindDraggable(service, "network-workgroup", svclctrl.MoveService, service.Button) drag.BindDraggable(&service, "network-workgroup", svclctrl.MoveService, service.Button)
return service return &service
} }
// SetRevealChild sets whether or not the service should reveal all sessions. // SetRevealChild sets whether or not the service should reveal all sessions.
@ -204,7 +198,7 @@ func (s *Service) AddSession(ses cchat.Session) *session.Row {
} }
func (s *Service) ID() string { func (s *Service) ID() string {
return s.service.Name().Content return s.service.ID()
} }
func (s *Service) Service() cchat.Service { func (s *Service) Service() cchat.Service {
@ -232,7 +226,7 @@ func (s *Service) MoveSession(id, movingID string) {
} }
func (s *Service) Breadcrumb() string { func (s *Service) Breadcrumb() string {
return s.service.Name().Content return s.Name.String()
} }
func (s *Service) ParentBreadcrumb() traverse.Breadcrumber { func (s *Service) ParentBreadcrumb() traverse.Breadcrumber {
@ -241,15 +235,13 @@ func (s *Service) ParentBreadcrumb() traverse.Breadcrumber {
func (s *Service) SaveAllSessions() { func (s *Service) SaveAllSessions() {
var sessions = s.BodyList.Sessions() var sessions = s.BodyList.Sessions()
var keyrings = make([]keyring.Session, 0, len(sessions)) var keyrings = keyring.NewService(s.service, len(sessions))
for _, s := range sessions { for _, session := range sessions {
if k := keyring.ConvertSession(s.Session); k != nil { keyrings.Add(session.Session, s.Name.String())
keyrings = append(keyrings, *k)
}
} }
keyring.SaveSessions(s.service, keyrings) keyrings.Save()
} }
func (s *Service) RestoreSession(row *session.Row, id string) { func (s *Service) RestoreSession(row *session.Row, id string) {
@ -265,7 +257,7 @@ func (s *Service) RestoreSession(row *session.Row, id string) {
log.Error(fmt.Errorf( log.Error(fmt.Errorf(
"Missing keyring for service %s, session ID %s", "Missing keyring for service %s, session ID %s",
s.service.Name().Content, id, s.service.ID(), id,
)) ))
} }
@ -276,8 +268,10 @@ func (s *Service) restoreAll() {
return return
} }
// Session is not a pointer, so we can pass it into arguments safely. service := keyring.Restore(s.service)
for _, ses := range keyring.RestoreSessions(s.service) {
for _, ses := range service.Sessions {
// Session is not a pointer, so we can pass it into arguments safely.
row := s.AddLoadingSession(ses.ID, ses.Name) row := s.AddLoadingSession(ses.ID, ses.Name)
row.RestoreSession(rs, ses) row.RestoreSession(rs, ses)
} }

View File

@ -1,11 +1,9 @@
package button package button
import ( import (
"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"
"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/text"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
@ -13,20 +11,32 @@ const UnreadColorDefs = `
@define-color mentioned rgb(240, 71, 71); @define-color mentioned rgb(240, 71, 71);
` `
type ToggleButtonImage struct { const IconSize = 38
*rich.ToggleButtonImage
type ToggleButton struct {
gtk.ToggleButton
Label *rich.Label
labelRev *gtk.Revealer
// These fields are nil if image is false.
Image *roundimage.StillImage
imageRev *gtk.Revealer
Box *gtk.Box
state rich.LabelStateStorer
clicked func(bool) clicked func(bool)
readcss primitives.ClassEnum readcss primitives.ClassEnum
err error icon string // whether or not the button has an icon
icon string // whether or not the button has an icon label bool
iconSz int
} }
var _ cchat.IconContainer = (*ToggleButtonImage)(nil)
var serverButtonCSS = primitives.PrepareClassCSS("server-button", ` var serverButtonCSS = primitives.PrepareClassCSS("server-button", `
.server-button {
min-width: 0px;
}
.selected-server { .selected-server {
border-left: 2px solid mix(@theme_base_color, @theme_fg_color, 0.1); border-left: 2px solid mix(@theme_base_color, @theme_fg_color, 0.1);
background-color: mix(@theme_base_color, @theme_fg_color, 0.1); background-color: mix(@theme_base_color, @theme_fg_color, 0.1);
@ -34,14 +44,14 @@ var serverButtonCSS = primitives.PrepareClassCSS("server-button", `
} }
.read { .read {
color: alpha(@theme_fg_color, 0.5); /* color: alpha(@theme_fg_color, 0.5); */
border-left: 2px solid transparent; border-left: 2px solid transparent;
} }
.unread { .unread {
color: @theme_fg_color; color: @theme_fg_color;
border-left: 2px solid alpha(@theme_fg_color, 0.75); border-left: 2px solid alpha(@theme_fg_color, 0.75);
background-color: alpha(@theme_fg_color, 0.05); /* background-color: alpha(@theme_fg_color, 0.05); */
} }
.mentioned { .mentioned {
@ -52,34 +62,55 @@ var serverButtonCSS = primitives.PrepareClassCSS("server-button", `
`+UnreadColorDefs) `+UnreadColorDefs)
func NewToggleButtonImage(content text.Rich) *ToggleButtonImage { // NewToggleButton creates a new toggle button.
b := rich.NewToggleButtonImage(content) func NewToggleButton(state rich.LabelStateStorer) *ToggleButton {
return WrapToggleButtonImage(b) label := rich.NewLabelWithRenderer(state, rich.RenderSkipImages)
} label.SetMarginStart(5)
label.Show()
func WrapToggleButtonImage(b *rich.ToggleButtonImage) *ToggleButtonImage { labelRev, _ := gtk.RevealerNew()
b.Show() labelRev.Add(label)
labelRev.SetRevealChild(true)
labelRev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
labelRev.SetTransitionDuration(100)
labelRev.Show()
tb := &ToggleButtonImage{ box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
ToggleButtonImage: b, box.SetHAlign(gtk.ALIGN_START)
clicked: func(bool) {}, box.PackStart(labelRev, false, false, 0)
} box.Show()
type activeGetter interface { button, _ := gtk.ToggleButtonNew()
GetActive() bool button.SetRelief(gtk.RELIEF_NONE)
button.Add(box)
button.Show()
tb := &ToggleButton{
ToggleButton: *button,
Label: label,
labelRev: labelRev,
Box: box,
state: state,
clicked: func(bool) {},
} }
tb.SetShowLabel(true)
tb.Connect("clicked", func(w *gtk.ToggleButton) { tb.clicked(w.GetActive()) }) tb.Connect("clicked", func(w *gtk.ToggleButton) { tb.clicked(w.GetActive()) })
serverButtonCSS(tb) serverButtonCSS(tb)
// Ensure that we display an icon when we receive one.
state.OnUpdate(func() {
if state.Image().HasImage() {
tb.ensureImage()
}
})
return tb return tb
} }
func (b *ToggleButtonImage) SetSelected(selected bool) { // SetSelected sets the button's intermediate state and appearance to look
// Set the clickability the opposite as the boolean. // like it's clicked without triggering the callback.
// b.SetSensitive(!selected) func (b *ToggleButton) SetSelected(selected bool) {
b.SetInconsistent(selected)
if selected { if selected {
primitives.AddClass(b, "selected-server") primitives.AddClass(b, "selected-server")
} else { } else {
@ -92,11 +123,36 @@ func (b *ToggleButtonImage) SetSelected(selected bool) {
} }
} }
func (b *ToggleButtonImage) SetClicked(clicked func(bool)) { // SetShowLabel sets whether or not to show the button's label. If the button
// does not have an image, then the label is always shown.
func (b *ToggleButton) SetShowLabel(showLabel bool) {
b.label = showLabel
// Enforce particular rules that are unfeasible at the moment. When these
// conditions change elsewhere, this function will be called again.
if b.imageRev != nil {
showLabel = showLabel || !b.imageRev.GetRevealChild()
} else {
showLabel = true
}
// Expand the box when we're showing the label.
b.SetHExpand(showLabel)
b.labelRev.SetRevealChild(showLabel)
}
// GetShowLabel gets whether or not the label is being shown.
func (b *ToggleButton) GetShowLabel() bool {
return b.labelRev.GetRevealChild() || b.label
}
// SetClicked sets the callback to run when clicked. It overrides the previous
// callback.
func (b *ToggleButton) SetClicked(clicked func(bool)) {
b.clicked = clicked b.clicked = clicked
} }
func (b *ToggleButtonImage) SetClickedIfTrue(clickedIfTrue func()) { func (b *ToggleButton) SetClickedIfTrue(clickedIfTrue func()) {
b.clicked = func(clicked bool) { b.clicked = func(clicked bool) {
if clicked { if clicked {
clickedIfTrue() clickedIfTrue()
@ -104,33 +160,74 @@ func (b *ToggleButtonImage) SetClickedIfTrue(clickedIfTrue func()) {
} }
} }
func (b *ToggleButtonImage) SetNormal() { func (b *ToggleButton) ensureImage() {
b.SetLabelUnsafe(b.GetLabel()) if b.Image != nil {
// b.menu.SetItems(b.extraMenu) return
}
if b.icon != "" { b.Image = roundimage.NewStillImage(b, 0)
b.Image.SetPlaceholderIcon(b.icon, b.Image.Size()) b.Image.SetSizeRequest(IconSize, IconSize)
b.Image.Show()
// TODO: tooltip false once hover is implemented.
rich.BindRoundImage(b.Image, b.state, true)
b.imageRev, _ = gtk.RevealerNew()
b.imageRev.Add(b.Image)
b.imageRev.SetRevealChild(true)
b.imageRev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
b.imageRev.SetTransitionDuration(75)
b.imageRev.Show()
b.Box.PackStart(b.imageRev, false, false, 0)
b.Box.ReorderChild(b.imageRev, 0)
// Set the callback to render the user's initials if there is no name.
b.Image.UseInitialsIfNone(func() string {
return b.state.Label().String()
})
// Restore the label's visible state now that we have an image.
b.SetShowLabel(b.label)
}
// UseEmptyIcon forces the ToggleButton to show an icon, even if it's a
// placeholder.
func (b *ToggleButton) UseEmptyIcon() {
b.ensureImage()
}
// SetNormal sets the button's state to normal from either loading or failed.
func (b *ToggleButton) SetNormal() {
b.Label.SetRenderer(rich.RenderSkipImages)
b.SetSensitive(true)
if b.Image != nil && b.icon != "" {
b.SetPlaceholderIcon(b.icon, b.Image.Size())
} }
} }
func (b *ToggleButtonImage) SetLoading() { // SetLoading sets the button's state to loading.
b.SetLabelUnsafe(b.GetLabel()) func (b *ToggleButton) SetLoading() {
b.Label.SetRenderer(rich.RenderSkipImages)
b.SetSensitive(false)
if b.icon != "" { if b.Image != nil && b.icon != "" {
b.Image.SetPlaceholderIcon("content-loading-symbolic", b.Image.Size()) b.SetPlaceholderIcon("content-loading-symbolic", b.Image.Size())
} }
} }
func (b *ToggleButtonImage) SetFailed(err error, retry func()) { func (b *ToggleButton) SetFailed(err error, retry func()) {
b.Label.SetMarkup(rich.MakeRed(b.GetLabel())) b.Label.SetRenderer(rich.MakeRed)
b.SetSensitive(true)
// If we have an icon set, then we can use the failed icon. // If we have an icon set, then we can use the failed icon.
if b.icon != "" { if b.Image != nil && b.icon != "" {
b.Image.SetPlaceholderIcon("computer-fail-symbolic", b.Image.Size()) b.SetPlaceholderIcon("computer-fail-symbolic", b.Image.Size())
} }
} }
func (b *ToggleButtonImage) SetUnreadUnsafe(unread, mentioned bool) { func (b *ToggleButton) SetUnreadUnsafe(unread, mentioned bool) {
switch { switch {
// Prioritize mentions over unreads. // Prioritize mentions over unreads.
case mentioned: case mentioned:
@ -142,16 +239,8 @@ func (b *ToggleButtonImage) SetUnreadUnsafe(unread, mentioned bool) {
} }
} }
func (b *ToggleButtonImage) SetPlaceholderIcon(iconName string, iconSzPx int) { func (b *ToggleButton) SetPlaceholderIcon(iconName string, iconSzPx int) {
b.icon = iconName b.icon = iconName
b.ensureImage()
b.Image.SetPlaceholderIcon(iconName, iconSzPx) b.Image.SetPlaceholderIcon(iconName, iconSzPx)
} }
func (b *ToggleButtonImage) SetIcon(url string) {
gts.ExecAsync(func() { b.SetIconUnsafe(url) })
}
func (b *ToggleButtonImage) SetIconUnsafe(url string) {
b.icon = ""
b.Image.SetIconUnsafe(url)
}

View File

@ -0,0 +1,3 @@
package button
type CollapsibleLabel struct{}

View File

@ -13,22 +13,28 @@ import (
) )
type Controller interface { type Controller interface {
ClearMessenger()
MessengerSelected(*ServerRow) MessengerSelected(*ServerRow)
// SelectColumnatedLister is called when the user clicks a server lister
// with its with Columnate method returning true. If lister is nil, then the
// impl should clear it.
SelectColumnatedLister(*ServerRow, cchat.Lister)
} }
// 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
// children will contain hollow rows. They are rows that do not yet have any // children will contain hollow rows. They are rows that do not yet have any
// widgets. This changes as soon as Row's Load is called. // widgets. This changes as soon as Row's Load is called.
type Children struct { type Children struct {
*gtk.Box gtk.Box
Controller
load *loading.Button // only not nil while loading load *loading.Button // only not nil while loading
loading bool loading bool
Rows []*ServerRow Rows []*ServerRow
expand bool
Parent traverse.Breadcrumber Parent traverse.Breadcrumber
rowctrl Controller
// Unreadable state for children rows to use. The parent row that has this // Unreadable state for children rows to use. The parent row that has this
// Children will bind a handler to this. // Children will bind a handler to this.
@ -37,8 +43,6 @@ type Children struct {
var childrenCSS = primitives.PrepareClassCSS("server-children", ` var childrenCSS = primitives.PrepareClassCSS("server-children", `
.server-children { .server-children {
margin: 0;
margin-top: 3px;
border-radius: 0; border-radius: 0;
} }
`) `)
@ -47,8 +51,8 @@ var childrenCSS = primitives.PrepareClassCSS("server-children", `
// widgets. // widgets.
func NewHollowChildren(p traverse.Breadcrumber, ctrl Controller) *Children { func NewHollowChildren(p traverse.Breadcrumber, ctrl Controller) *Children {
return &Children{ return &Children{
Parent: p, Parent: p,
rowctrl: ctrl, Controller: ctrl,
} }
} }
@ -60,7 +64,7 @@ func NewChildren(p traverse.Breadcrumber, ctrl Controller) *Children {
} }
func (c *Children) IsHollow() bool { func (c *Children) IsHollow() bool {
return c.Box == nil return c.Box.Object == nil
} }
// Init ensures that the children container is not hollow. It does nothing after // Init ensures that the children container is not hollow. It does nothing after
@ -70,10 +74,13 @@ func (c *Children) IsHollow() bool {
// Nothing but ServerRow should call this method. // Nothing but ServerRow should call this method.
func (c *Children) Init() { func (c *Children) Init() {
if c.IsHollow() { if c.IsHollow() {
c.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
c.Box.SetMarginStart(ChildrenMargin) box.SetMarginStart(ChildrenMargin)
c.Box.SetHExpand(true) c.Box = *box
childrenCSS(c.Box) childrenCSS(box)
// Show all margins by default.
c.SetExpand(true)
// Check if we're still loading. This is effectively restoring the // Check if we're still loading. This is effectively restoring the
// state that was set before we had widgets. // state that was set before we had widgets.
@ -90,7 +97,7 @@ func (c *Children) Init() {
func (c *Children) Reset() { func (c *Children) Reset() {
// If the children container isn't hollow, then we have to remove the known // If the children container isn't hollow, then we have to remove the known
// rows from the container box. // rows from the container box.
if c.Box != nil { if c.Box.Object != nil {
// Remove old servers from the list. // Remove old servers from the list.
for _, row := range c.Rows { for _, row := range c.Rows {
if row.IsHollow() { if row.IsHollow() {
@ -104,6 +111,26 @@ func (c *Children) Reset() {
c.Rows = nil c.Rows = nil
} }
// SetExpand sets whether or not to expand the margin and show the labels.
func (c *Children) SetExpand(expand bool) {
AssertUnhollow(c)
c.expand = expand
if expand {
primitives.AddClass(c, "expand")
} else {
primitives.RemoveClass(c, "expand")
}
for _, row := range c.Rows {
// Ensure that the row is initialized. If we're expanded, then show the
// label.
if row.Button != nil {
row.SetShowLabel(expand)
}
}
}
// setLoading shows the loading circle as a list child. If hollow, this function // setLoading shows the loading circle as a list child. If hollow, this function
// will only update the state. // will only update the state.
func (c *Children) setLoading() { func (c *Children) setLoading() {
@ -168,7 +195,7 @@ func (c *Children) SetServersUnsafe(servers []cchat.Server) {
if server == nil { if server == nil {
log.Panicln("one of given servers in SetServers is nil at ", i) log.Panicln("one of given servers in SetServers is nil at ", i)
} }
c.Rows[i] = NewHollowServer(c, server, c.rowctrl) c.Rows[i] = NewHollowServer(c, server, c)
} }
// We should not unhollow everything here, but rather on uncollapse. // We should not unhollow everything here, but rather on uncollapse.
@ -205,7 +232,7 @@ func (c *Children) UpdateServerUnsafe(update cchat.ServerUpdate) {
prevID, replace := update.PreviousID() prevID, replace := update.PreviousID()
// TODO: I don't think this code unhollows a new server. // TODO: I don't think this code unhollows a new server.
var newServer = NewHollowServer(c, update, c.rowctrl) var newServer = NewHollowServer(c, update, c)
var i, oldRow = c.findID(prevID) var i, oldRow = c.findID(prevID)
// If we're appending a new row, then replace is false. // If we're appending a new row, then replace is false.
@ -246,23 +273,13 @@ func (c *Children) LoadAll() {
// Restore expansion if possible. // Restore expansion if possible.
savepath.Restore(row, row.Button) savepath.Restore(row, row.Button)
} }
}
// Check if we have icons. // ForceIcons forces all of the children's row to show icons.
var hasIcon bool func (c *Children) ForceIcons() {
for _, row := range c.Rows { for _, row := range c.Rows {
if row.HasIcon() { row.UseEmptyIcon()
hasIcon = true row.SetShowLabel(c.expand)
break
}
}
// If we have an icon, then show all other possibly empty icons. HdyAvatar
// will generate a placeholder.
if hasIcon {
for _, row := range c.Rows {
row.UseEmptyIcon()
}
} }
} }

View File

@ -5,19 +5,20 @@ import (
"fmt" "fmt"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
// Buffer represents an unbuffered API around the text buffer. // Buffer represents an unbuffered API around the text buffer.
type Buffer struct { type Buffer struct {
*gtk.TextBuffer *gtk.TextBuffer
name string name rich.LabelStateStorer
cmder cchat.Commander 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(name string, cmder cchat.Commander) *Buffer { func NewBuffer(name rich.LabelStateStorer, cmder cchat.Commander) *Buffer {
if cmder == nil { if cmder == nil {
return nil return nil
} }

View File

@ -39,10 +39,14 @@ func SpawnDialog(buf *Buffer) {
s.Show() s.Show()
h, _ := gtk.HeaderBarNew() h, _ := gtk.HeaderBarNew()
h.SetTitle("Commander: " + buf.name)
h.SetShowCloseButton(true) h.SetShowCloseButton(true)
h.Show() h.Show()
rm := buf.name.OnUpdate(func() {
h.SetTitle("Commander: " + buf.name.Label().Content)
})
h.Connect("destroy", rm)
d, _ := gts.NewEmptyModalDialog() d, _ := gts.NewEmptyModalDialog()
d.SetDefaultSize(450, 250) d.SetDefaultSize(450, 250)
d.SetTitlebar(h) d.SetTitlebar(h)

View File

@ -1,25 +1,24 @@
package server package server
import ( import (
"context"
"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/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/actions"
"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/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/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const ChildrenMargin = 24 const ChildrenMargin = 0 // refer to style.css
const IconSize = 32
func AssertUnhollow(hollower interface{ IsHollow() bool }) { func AssertUnhollow(hollower interface{ IsHollow() bool }) {
if hollower.IsHollow() { if hollower.IsHollow() {
@ -27,14 +26,21 @@ func AssertUnhollow(hollower interface{ IsHollow() bool }) {
} }
} }
// ParentController controls ServerRow's container, which is the Children
// struct.
type ParentController interface {
Controller
ForceIcons()
}
type ServerRow struct { type ServerRow struct {
*gtk.Box *gtk.Box
Avatar *roundimage.Avatar Button *button.ToggleButton
Button *button.ToggleButtonImage
ActionsMenu *actions.Menu ActionsMenu *actions.Menu
Server cchat.Server Server cchat.Server
ctrl Controller name rich.NameContainer
ctrl ParentController
parentcrumb traverse.Breadcrumber parentcrumb traverse.Breadcrumber
@ -46,10 +52,12 @@ type ServerRow struct {
childrev *gtk.Revealer childrev *gtk.Revealer
children *Children children *Children
serverList cchat.Lister serverList cchat.Lister
serverStop func()
// 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
mentioned bool mentioned bool
showLabel bool
// callback to cancel unread indicator // callback to cancel unread indicator
cancelUnread func() cancelUnread func()
@ -59,25 +67,28 @@ var serverCSS = primitives.PrepareClassCSS("server", `
/* Ignore first child because .server-children already covers this */ /* Ignore first child because .server-children already covers this */
.server:not(:first-child) { .server:not(:first-child) {
margin: 0; margin: 0;
margin-top: 3px;
border-radius: 0; border-radius: 0;
} }
.server.active-column {
background-color: mix(@theme_bg_color, @theme_selected_bg_color, 0.25);
}
`) `)
// NewHollowServer creates a new hollow ServerRow. It will automatically create // NewHollowServer creates a new hollow ServerRow. It will automatically create
// hollow children containers and rows for the given server. // hollow children containers and rows for the given server.
func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl Controller) *ServerRow { func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl ParentController) *ServerRow {
var serverRow = &ServerRow{ serverRow := ServerRow{
parentcrumb: p, parentcrumb: p,
ctrl: ctrl, ctrl: ctrl,
Server: sv, Server: sv,
cancelUnread: func() {}, cancelUnread: func() {},
} }
var ( serverRow.name.QueueNamer(context.Background(), sv)
lister = sv.AsLister()
messenger = sv.AsMessenger() lister := sv.AsLister()
) messenger := sv.AsMessenger()
switch { switch {
case lister != nil: case lister != nil:
@ -87,7 +98,7 @@ func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl Controller)
case messenger != nil: case messenger != nil:
if unreader := messenger.AsUnreadIndicator(); unreader != nil { 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 {
return nil, errors.Wrap(err, "Failed to use unread indicator") return nil, errors.Wrap(err, "Failed to use unread indicator")
} }
@ -97,7 +108,7 @@ func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl Controller)
} }
} }
return serverRow return &serverRow
} }
// Init brings the row out of the hollow state. It loads the children (if any), // Init brings the row out of the hollow state. It loads the children (if any),
@ -108,19 +119,13 @@ func (r *ServerRow) Init() {
} }
// Initialize the row, which would fill up the button and others as well. // Initialize the row, which would fill up the button and others as well.
r.Avatar = roundimage.NewAvatar(IconSize)
r.Avatar.SetText(r.Server.Name().Content)
r.Avatar.Show()
btn := rich.NewCustomToggleButtonImage(r.Avatar, r.Server.Name()) r.Button = button.NewToggleButton(&r.name)
btn.Show() r.Button.SetShowLabel(r.showLabel)
r.Button = button.WrapToggleButtonImage(btn)
r.Button.Box.SetHAlign(gtk.ALIGN_START)
r.Button.SetRelief(gtk.RELIEF_NONE)
r.Button.Show() r.Button.Show()
r.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) r.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
r.Box.SetHAlign(gtk.ALIGN_FILL)
r.Box.PackStart(r.Button, false, false, 0) r.Box.PackStart(r.Button, false, false, 0)
serverCSS(r.Box) serverCSS(r.Box)
@ -131,9 +136,6 @@ func (r *ServerRow) Init() {
// Ensure errors are displayed. // Ensure errors are displayed.
r.childrenSetErr(r.childrenErr) r.childrenSetErr(r.childrenErr)
// Try to set an icon.
r.SetIconer(r.Server)
// Connect the destroyer, if any. // Connect the destroyer, if any.
r.Connect("destroy", func(interface{}) { r.cancelUnread() }) r.Connect("destroy", func(interface{}) { r.cancelUnread() })
@ -141,7 +143,7 @@ func (r *ServerRow) Init() {
r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state
if cmder := r.Server.AsCommander(); cmder != nil { if cmder := r.Server.AsCommander(); cmder != nil {
r.cmder = commander.NewBuffer(r.Server.Name().String(), cmder) r.cmder = commander.NewBuffer(&r.name, cmder)
r.ActionsMenu.AddAction("Command Prompt", r.cmder.ShowDialog) r.ActionsMenu.AddAction("Command Prompt", r.cmder.ShowDialog)
} }
@ -152,13 +154,21 @@ func (r *ServerRow) Init() {
} }
}) })
// Bring up the icons of all the current level's rows if we have one.
r.name.OnUpdate(func() {
if r.name.Image().HasImage() {
r.ctrl.ForceIcons()
}
})
var ( var (
lister = r.Server.AsLister() lister = r.Server.AsLister()
columnate = lister != nil && lister.Columnate()
messenger = r.Server.AsMessenger() messenger = r.Server.AsMessenger()
) )
switch { switch {
case lister != nil: case lister != nil && !columnate:
primitives.AddClass(r, "server-list") primitives.AddClass(r, "server-list")
r.children.Init() r.children.Init()
r.children.Show() r.children.Show()
@ -171,15 +181,35 @@ 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 lister != nil && columnate:
primitives.AddClass(r, "server-list")
primitives.AddClass(r, "server-columnate")
r.Button.SetClicked(func(active bool) {
if active {
r.ctrl.SelectColumnatedLister(r, lister)
} else {
r.ctrl.SelectColumnatedLister(r, nil)
}
})
case messenger != nil: case messenger != nil:
primitives.AddClass(r, "server-message") primitives.AddClass(r, "server-message")
r.Button.SetClicked(func(bool) { r.ctrl.MessengerSelected(r) }) r.Button.SetClicked(func(active bool) {
if active {
r.ctrl.MessengerSelected(r)
} else {
r.ctrl.ClearMessenger()
}
})
} }
// Restore the label visibility state.
r.SetShowLabel(r.showLabel)
} }
// GetActiveServerMessage returns true if the row is currently selected AND it // IsActiveServerMessage returns true if the row is currently selected AND it
// is a message row. // is a message row.
func (r *ServerRow) GetActiveServerMessage() bool { func (r *ServerRow) IsActiveServerMessage() bool {
// If the button is nil, then that probably means we're still in a hollow // If the button is nil, then that probably means we're still in a hollow
// state. This obviously means nothing is being selected. // state. This obviously means nothing is being selected.
if r.Button == nil { if r.Button == nil {
@ -196,7 +226,7 @@ func (r *ServerRow) SetUnread(unread, mentioned bool) {
func (r *ServerRow) SetUnreadUnsafe(unread, mentioned bool) { func (r *ServerRow) SetUnreadUnsafe(unread, mentioned bool) {
// We're never unread if we're reading this current server. // We're never unread if we're reading this current server.
if r.GetActiveServerMessage() { if r.IsActiveServerMessage() {
unread, mentioned = false, false unread, mentioned = false, false
} }
@ -243,12 +273,14 @@ func (r *ServerRow) load(finish func(error)) {
} }
go func() { go func() {
var err = list.Servers(children) stop, err := list.Servers(children)
if err != nil { if err != nil {
log.Error(errors.Wrap(err, "Failed to get servers")) log.Error(errors.Wrap(err, "Failed to get servers"))
} }
gts.ExecAsync(func() { gts.ExecAsync(func() {
r.serverStop = stop
// Announce that we're not loading anymore. // Announce that we're not loading anymore.
r.children.setNotLoading() r.children.setNotLoading()
@ -273,6 +305,11 @@ func (r *ServerRow) Reset() {
r.children.Destroy() r.children.Destroy()
} }
if r.serverStop != nil {
r.serverStop()
r.serverStop = nil
}
// Reset the state. // Reset the state.
r.ActionsMenu.Reset() r.ActionsMenu.Reset()
r.serverList = nil r.serverList = nil
@ -298,14 +335,36 @@ func (r *ServerRow) childrenSetErr(err error) {
// UseEmptyIcon forces the row to show a placeholder icon. // UseEmptyIcon forces the row to show a placeholder icon.
func (r *ServerRow) UseEmptyIcon() { func (r *ServerRow) UseEmptyIcon() {
AssertUnhollow(r) AssertUnhollow(r)
r.Button.UseEmptyIcon()
r.Button.Image.SetSize(IconSize)
r.Button.Image.SetRevealChild(true)
} }
// HasIcon returns true if the current row has an icon. // HasIcon returns true if the current row has an icon.
func (r *ServerRow) HasIcon() bool { func (r *ServerRow) HasIcon() bool {
return !r.IsHollow() && r.Button.Image.GetRevealChild() return !r.IsHollow() && r.Button.Image != nil
}
// SetShowLabel sets whether or not to show the button's (and its children's, if
// any)'s icons.
func (r *ServerRow) SetShowLabel(showLabel bool) {
r.showLabel = showLabel
if r.IsHollow() {
return
}
r.Button.SetShowLabel(showLabel)
// We'd want the button to be wide if we're showing the label. Otherwise,
// it can be small.
if r.Button.GetShowLabel() {
r.Box.SetHAlign(gtk.ALIGN_FILL)
} else {
r.Box.SetHAlign(gtk.ALIGN_START)
}
if r.children != nil && !r.children.IsHollow() {
r.children.SetExpand(showLabel)
}
} }
func (r *ServerRow) ParentBreadcrumb() traverse.Breadcrumber { func (r *ServerRow) ParentBreadcrumb() traverse.Breadcrumber {
@ -316,28 +375,18 @@ func (r *ServerRow) Breadcrumb() string {
if r.IsHollow() { if r.IsHollow() {
return "" return ""
} }
return r.Button.GetText()
return r.name.String()
} }
// ID returns the server ID.
func (r *ServerRow) ID() cchat.ID { func (r *ServerRow) ID() cchat.ID {
return r.Server.ID() return r.Server.ID()
} }
func (r *ServerRow) SetLabelUnsafe(name text.Rich) { // Name returns the name state.
AssertUnhollow(r) func (r *ServerRow) Name() rich.LabelStateStorer {
return &r.name
r.Button.SetLabelUnsafe(name)
r.Avatar.SetText(name.Content)
}
// SetIconer takes in a Namer for AsIconer.
func (r *ServerRow) SetIconer(v cchat.Namer) {
AssertUnhollow(r)
if iconer := v.AsIconer(); iconer != nil {
r.Button.Image.SetSize(IconSize)
r.Button.Image.AsyncSetIconer(iconer, "Error getting server icon URL")
}
} }
// SetLoading is called by the parent struct. // SetLoading is called by the parent struct.
@ -357,7 +406,6 @@ func (r *ServerRow) SetFailed(err error, retry func()) {
r.SetSensitive(true) r.SetSensitive(true)
r.SetTooltipText(err.Error()) r.SetTooltipText(err.Error())
r.Button.SetFailed(err, retry) r.Button.SetFailed(err, retry)
r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel()))
r.ActionsMenu.Reset() r.ActionsMenu.Reset()
r.ActionsMenu.AddAction("Retry", retry) r.ActionsMenu.AddAction("Retry", retry)
} }
@ -372,14 +420,6 @@ func (r *ServerRow) SetDone() {
r.SetTooltipText("") r.SetTooltipText("")
} }
// func (r *ServerRow) SetNormalExtraMenu(items []menu.Item) {
// AssertUnhollow(r)
// r.Button.SetNormalExtraMenu(items)
// r.SetSensitive(true)
// r.SetTooltipText("")
// }
// SetSelected is used for highlighting the current message server. // SetSelected is used for highlighting the current message server.
func (r *ServerRow) SetSelected(selected bool) { func (r *ServerRow) SetSelected(selected bool) {
AssertUnhollow(r) AssertUnhollow(r)
@ -411,6 +451,12 @@ func (r *ServerRow) SetRevealChild(reveal bool) {
// Save the path. // Save the path.
savepath.Update(r, reveal) savepath.Update(r, reveal)
if reveal {
primitives.AddClass(r, "expanded")
} else {
primitives.RemoveClass(r, "expanded")
}
// If this isn't a reveal, then we don't need to load. // If this isn't a reveal, then we don't need to load.
if !reveal { if !reveal {
return return

View File

@ -22,6 +22,8 @@ type Breadcrumber interface {
type BreadcrumbNamer interface { type BreadcrumbNamer interface {
// Breadcrumb returns the breadcrumb name. // Breadcrumb returns the breadcrumb name.
Breadcrumb() string Breadcrumb() string
// TODO: make BreadcrumbNamer return LabelState.
} }
// Traverse traverses the given breadcrumber recursively. If traverser returns // Traverse traverses the given breadcrumber recursively. If traverser returns

View File

@ -0,0 +1,64 @@
package serverpane
import (
"github.com/gotk3/gotk3/gtk"
)
// Paned replaces gtk.Paned or gtk.Box to allow an optional second pane.
type Paned struct {
gtk.IWidget
Box *gtk.Box
orien gtk.Orientation
w1 gtk.IWidget
w2 gtk.IWidget
}
// NewPaned creates a new empty pane.
func NewPaned(w1 gtk.IWidget, o gtk.Orientation) *Paned {
// box holds either paned or w1.
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(w1, true, true, 0)
return &Paned{
IWidget: box,
Box: box,
orien: o,
w1: w1,
}
}
func (p *Paned) Destroy() {
p.Box.Destroy()
*p = Paned{}
}
func (p *Paned) Show() {
p.Box.Show()
}
// AddSide adds a side widget. If a second widget is already added, then it is
// removed from the pane.
func (p *Paned) AddSide(w gtk.IWidget) {
if p.w2 != nil {
p.Box.Remove(p.w2)
}
p.w2 = w
p.Box.PackStart(p.w2, true, true, 0)
p.Box.SetChildPacking(p.w1, false, false, 0, gtk.PACK_START)
}
// Remove removes either w1 or w2. If neither matches, then nothing is done.
func (p *Paned) Remove(w gtk.IWidget) {
switch w {
case p.w1:
panic("p.w1 must not be removed")
case p.w2:
p.Box.Remove(p.w2)
p.w2 = nil
p.Box.SetChildPacking(p.w1, true, true, 0, gtk.PACK_START)
}
}

View File

@ -10,6 +10,7 @@ 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/service/session/server" "github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse" "github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/serverpane"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango" "github.com/gotk3/gotk3/pango"
) )
@ -20,59 +21,102 @@ const ListWidth = 200
// SessionController extends server.Controller to add needed methods that the // SessionController extends server.Controller to add needed methods that the
// specific top-level servers container needs. // specific top-level servers container needs.
type SessionController interface { type SessionController interface {
server.Controller
ClearMessenger() ClearMessenger()
MessengerSelected(*server.ServerRow)
} }
// Servers wraps around a list of servers inherited from Children. It's the // Servers wraps around a list of servers inherited from Children to display a
// container that's displayed on the right of the service sidebar. // Lister in its own box instead of as a nested list. It's the container that's
// displayed on the right of the service sidebar.
type Servers struct { type Servers struct {
*gtk.Box gtk.Stack
SessionController
spinner *spinner.Boxed
// Main is the horizontal box containing the current struct's list of
// servers columnated with the same level. The second item in the box should
// be the selected server.
Main *serverpane.Paned
// Lister is the current lister belonging to this server.
Lister cchat.Lister
stopLs func()
// Children is main's lhs.
Children *server.Children Children *server.Children
spinner *spinner.Boxed // non-nil if loading.
ctrl SessionController // NextColumn is main's rhs.
NextColumn *Servers // nil
// state detachNext func()
ServerList cchat.Lister
} }
var toplevelCSS = primitives.PrepareClassCSS("top-level", ` var toplevelCSS = primitives.PrepareClassCSS("top-level", `
.top-level { .top-level {
margin: 0 3px;
margin-top: 3px;
} }
`) `)
// NewServers creates a new Servers instance that holds only the given column
// number and its children. Any servers with a different columnate ID will be in
// the children pane.
func NewServers(p traverse.Breadcrumber, ctrl SessionController) *Servers { func NewServers(p traverse.Breadcrumber, ctrl SessionController) *Servers {
c := server.NewChildren(p, ctrl) servers := Servers{
c.SetMarginStart(0) // children is top level; there is no main row SessionController: ctrl,
c.SetVExpand(true)
c.Show()
toplevelCSS(c)
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
return &Servers{
Box: b,
Children: c,
ctrl: ctrl,
} }
servers.Children = server.NewChildren(p, &servers)
servers.Children.SetVExpand(true)
servers.Children.Show()
toplevelCSS(servers.Children)
servers.Main = serverpane.NewPaned(servers.Children, gtk.ORIENTATION_VERTICAL)
servers.Main.Show()
stack, _ := gtk.StackNew()
servers.Stack = *stack
servers.Stack.SetVAlign(gtk.ALIGN_START)
servers.Stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
servers.Stack.SetTransitionDuration(75)
servers.Stack.AddNamed(servers.Main, "main")
servers.Stack.Show()
return &servers
}
// Destroy destroys and invalidates this instance permanently.
func (s *Servers) Destroy() {
s.Reset()
s.Stack.Destroy()
} }
func (s *Servers) Reset() { func (s *Servers) Reset() {
// Reset isn't necessarily called while loading, so we do a check. // Reset isn't necessarily called while loading, so we do a check.
if s.spinner != nil { if s.spinner != nil {
s.spinner.Stop() s.spinner.Destroy()
s.spinner = nil s.spinner = nil
} }
// Close the right server column if any.
if s.NextColumn != nil {
if s.detachNext != nil {
s.detachNext()
}
s.Main.Remove(s.NextColumn)
s.NextColumn.Destroy()
s.NextColumn = nil
}
// Call the destructor if any.
if s.stopLs != nil {
s.stopLs()
s.stopLs = nil
}
// Reset the state. // Reset the state.
s.ServerList = nil s.Lister = nil
// Remove all children.
primitives.RemoveChildren(s)
// Reset the children container. // Reset the children container.
s.Children.Reset() s.Children.Reset()
s.Stack.SetVisibleChild(s.Main)
} }
// IsLoading returns true if the servers container is loading. // IsLoading returns true if the servers container is loading.
@ -83,8 +127,12 @@ 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.Lister) { func (s *Servers) SetList(slist cchat.Lister) {
primitives.RemoveChildren(s) if s.stopLs != nil {
s.ServerList = slist s.stopLs()
s.stopLs = nil
}
s.Lister = slist
s.load() s.load()
} }
@ -98,11 +146,12 @@ func (s *Servers) load() {
s.setLoading() s.setLoading()
go func() { go func() {
err := s.ServerList.Servers(s) stop, err := s.Lister.Servers(s)
gts.ExecAsync(func() { gts.ExecAsync(func() {
if err != nil { if err != nil {
s.setFailed(err) s.setFailed(err)
} else { } else {
s.stopLs = stop
s.setDone() s.setDone()
} }
}) })
@ -114,8 +163,8 @@ func (s *Servers) SetServers(servers []cchat.Server) {
gts.ExecAsync(func() { gts.ExecAsync(func() {
s.Children.SetServersUnsafe(servers) s.Children.SetServersUnsafe(servers)
if len(servers) == 0 { if servers == nil {
s.ctrl.ClearMessenger() s.ClearMessenger()
return return
} }
@ -131,37 +180,38 @@ func (s *Servers) UpdateServer(update cchat.ServerUpdate) {
// setDone changes the view to show the servers. // setDone changes the view to show the servers.
func (s *Servers) setDone() { func (s *Servers) setDone() {
primitives.RemoveChildren(s) s.SetVisibleChild(s.Main)
// stop the spinner. // stop the spinner.
s.spinner.Stop() s.spinner.Destroy()
s.spinner = nil s.spinner = nil
s.Add(s.Children)
} }
// setLoading shows a loading spinner. Use this after the session row is // setLoading shows a loading spinner. Use this after the session row is
// connected. // connected.
func (s *Servers) setLoading() { func (s *Servers) setLoading() {
primitives.RemoveChildren(s)
s.spinner = spinner.New() s.spinner = spinner.New()
s.spinner.SetSizeRequest(FaceSize, FaceSize) s.spinner.SetSizeRequest(FaceSize, FaceSize)
s.spinner.Show() s.spinner.Show()
s.spinner.Start() s.spinner.Start()
s.Add(s.spinner) s.AddNamed(s.spinner, "spinner")
s.SetVisibleChildName("spinner")
} }
// setFailed shows a sad face with the error. Use this when the session row has // setFailed shows a sad face with the error. Use this when the session row has
// failed to load. // failed to load.
func (s *Servers) setFailed(err error) { func (s *Servers) setFailed(err error) {
primitives.RemoveChildren(s) // stop the spinner. Let this SEGFAULT if nil.
s.spinner.Destroy()
// stop the spinner. Let this SEGFAULT if nil, as that's undefined behavior.
s.spinner.Stop()
s.spinner = nil s.spinner = nil
// Remove existing error widgets.
w, err := s.Stack.GetChildByName("error")
if err == nil {
s.Stack.Remove(w)
}
// Create a BLANK label for padding. // Create a BLANK label for padding.
ltop, _ := gtk.LabelNew("") ltop, _ := gtk.LabelNew("")
ltop.Show() ltop.Show()
@ -182,7 +232,49 @@ func (s *Servers) setFailed(err error) {
lerr.Show() lerr.Show()
// Add these items into the box. // Add these items into the box.
s.PackStart(ltop, false, false, 0) b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
s.PackStart(btn, false, false, 10) // pad b.PackStart(ltop, false, false, 0)
s.PackStart(lerr, false, false, 0) b.PackStart(btn, false, false, 10) // pad
b.PackStart(lerr, false, false, 0)
b.Show()
s.Stack.AddNamed(b, "error")
s.Stack.SetVisibleChildName("error")
}
// SelectColumnatedLister is called by children servers to open up a server list
// on the right.
func (s *Servers) SelectColumnatedLister(srv *server.ServerRow, lst cchat.Lister) {
if s.detachNext != nil {
s.Main.Remove(s.NextColumn) // run the deconstructor
s.detachNext()
s.NextColumn.Destroy()
}
if lst == nil {
return
}
s.NextColumn = NewServers(srv, s)
s.NextColumn.SetList(lst)
s.Main.AddSide(s.NextColumn)
update := func(box *gtk.Box) {
a := box.GetAllocation()
// Align the next column to the selected item.
s.NextColumn.SetMarginTop(a.GetY())
}
s.Children.SetExpand(false)
primitives.AddClass(srv, "active-column")
update(srv.Box)
sizeHandle := srv.Box.Connect("size-allocate", update)
s.detachNext = func() {
srv.Box.HandlerDisconnect(sizeHandle)
s.Children.SetExpand(true)
primitives.RemoveClass(srv, "active-column")
}
} }

View File

@ -1,6 +1,8 @@
package session package session
import ( import (
"context"
"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/keyring" "github.com/diamondburned/cchat-gtk/internal/keyring"
@ -9,9 +11,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/actions" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/actions"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag"
"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/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/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/commander"
@ -55,9 +55,9 @@ type Controller interface {
// Row represents a session row entry in the session List. // Row represents a session row entry in the session List.
type Row struct { type Row struct {
*gtk.ListBoxRow *gtk.ListBoxRow
avatar *roundimage.Avatar name rich.NameContainer
iconBox *gtk.EventBox iconBox *gtk.EventBox
icon *rich.Icon // nillable icon *roundimage.StillImage // nillable
ctrl Controller ctrl Controller
parentcrumb traverse.Breadcrumber parentcrumb traverse.Breadcrumber
@ -80,6 +80,10 @@ type Row struct {
var rowCSS = primitives.PrepareClassCSS("session-row", var rowCSS = primitives.PrepareClassCSS("session-row",
button.UnreadColorDefs+` button.UnreadColorDefs+`
.session-row {
padding: 6px;
}
.session-row:last-child { .session-row:last-child {
border-radius: 0 0 14px 14px; border-radius: 0 0 14px 14px;
} }
@ -117,32 +121,21 @@ var rowCSS = primitives.PrepareClassCSS("session-row",
} }
`) `)
var rowIconCSS = primitives.PrepareClassCSS("session-icon", ` var rowIconCSS = primitives.PrepareClassCSS("session-icon", ``)
.session-icon {
padding: 4px;
margin: 0;
}
`)
const IconSize = 48 const (
const IconName = "face-plain-symbolic" IconSize = 42
IconName = "face-plain-symbolic"
func newIcon(img rich.RoundIconContainer) *rich.Icon { )
icon := rich.NewCustomIcon(img, IconSize)
icon.SetPlaceholderIcon(IconName, IconSize)
icon.ShowAll()
rowIconCSS(icon)
return icon
}
func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Controller) *Row { func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Controller) *Row {
row := newRow(parent, text.Rich{}, ctrl) row := newRow(parent, text.Plain(""), ctrl)
row.SetSession(ses) row.SetSession(ses)
return row return row
} }
func NewLoading(parent traverse.Breadcrumber, id, name string, ctrl Controller) *Row { func NewLoading(parent traverse.Breadcrumber, id, name string, ctrl Controller) *Row {
row := newRow(parent, text.Rich{Content: name}, ctrl) row := newRow(parent, text.Plain(name), ctrl)
row.sessionID = id row.sessionID = id
row.SetLoading() row.SetLoading()
return row return row
@ -154,17 +147,32 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Controller) *Row
parentcrumb: parent, parentcrumb: parent,
} }
row.avatar = roundimage.NewAvatar(IconSize) if !name.IsEmpty() {
row.avatar.SetText(name.Content) row.name.LabelState = *rich.NewLabelState(name)
row.avatar.Show() }
row.iconBox, _ = gtk.EventBoxNew() row.iconBox, _ = gtk.EventBoxNew()
row.iconBox.Show() row.iconBox.Show()
row.icon = roundimage.NewStillImage(row.iconBox, 0)
row.icon.SetSizeRequest(IconSize, IconSize)
row.icon.SetPlaceholderIcon(IconName, IconSize/2)
row.icon.Show()
rowIconCSS(row.icon)
row.iconBox.Add(row.icon)
rich.BindRoundImage(row.icon, &row.name, true)
row.ListBoxRow, _ = gtk.ListBoxRowNew() row.ListBoxRow, _ = gtk.ListBoxRowNew()
row.ListBoxRow.Add(row.iconBox)
row.ListBoxRow.Show() row.ListBoxRow.Show()
rowCSS(row.ListBoxRow) rowCSS(row.ListBoxRow)
row.name.OnUpdate(func() {
// TODO: proper popovers instead of tooltips.
row.ListBoxRow.SetTooltipText(row.name.String())
})
// TODO: commander button // TODO: commander button
row.Servers = NewServers(row, row) row.Servers = NewServers(row, row)
@ -216,6 +224,11 @@ func NewAddButton() *gtk.ListBoxRow {
return row return row
} }
// Name returns the session row's name container.
func (r *Row) Name() rich.LabelStateStorer {
return &r.name
}
// Reset extends the server row's Reset function and resets additional states. // Reset extends the server row's Reset function and resets additional states.
// It resets all states back to nil, but the session ID stays. // It resets all states back to nil, but the session ID stays.
func (r *Row) Reset() { func (r *Row) Reset() {
@ -223,14 +236,10 @@ func (r *Row) Reset() {
r.ActionsMenu.Reset() // wipe menu items r.ActionsMenu.Reset() // wipe menu items
r.ActionsMenu.AddAction("Remove", r.RemoveSession) r.ActionsMenu.AddAction("Remove", r.RemoveSession)
if r.icon == nil {
r.icon = newIcon(r.avatar)
r.iconBox.Add(r.icon)
}
// Set a lame placeholder icon. // Set a lame placeholder icon.
r.icon.SetPlaceholderIcon("folder-remote-symbolic", IconSize) r.icon.SetPlaceholderIcon("folder-remote-symbolic", IconSize)
r.name.Stop()
r.Session = nil r.Session = nil
r.cmder = nil r.cmder = nil
} }
@ -243,7 +252,8 @@ func (r *Row) Breadcrumb() string {
if r.Session == nil { if r.Session == nil {
return "" return ""
} }
return r.Session.Name().Content
return r.name.String()
} }
func (r *Row) ClearMessenger() { func (r *Row) ClearMessenger() {
@ -273,25 +283,11 @@ func (r *Row) Activate() {
func (r *Row) SetLoading() { func (r *Row) SetLoading() {
// Reset the state. // Reset the state.
r.Session = nil r.Session = nil
r.name.Stop()
// Reset the icon.
primitives.RemoveChildren(r.iconBox)
r.icon = nil
// Remove everything from the row, including the icon.
primitives.RemoveChildren(r)
// Remove the failed class. // Remove the failed class.
primitives.RemoveClass(r, "failed") primitives.RemoveClass(r, "failed")
// Add a loading circle.
spin := spinner.New()
spin.SetSizeRequest(IconSize, IconSize)
spin.Start()
spin.Show()
rowIconCSS(spin)
r.Add(spin)
r.SetSensitive(false) // no activate r.SetSensitive(false) // no activate
} }
@ -303,18 +299,8 @@ func (r *Row) SetFailed(err error) {
r.Session = nil r.Session = nil
// Re-enable the row. // Re-enable the row.
r.SetSensitive(true) r.SetSensitive(true)
// Remove everything off the row.
primitives.RemoveChildren(r)
// Mark the row as failed. // Mark the row as failed.
primitives.AddClass(r, "failed") primitives.AddClass(r, "failed")
if r.icon == nil {
r.icon = newIcon(r.avatar)
r.iconBox.Add(r.icon)
}
// Add the icon.
r.Add(r.iconBox)
// Set the button to a retry icon. // Set the button to a retry icon.
r.icon.SetPlaceholderIcon("view-refresh-symbolic", IconSize) r.icon.SetPlaceholderIcon("view-refresh-symbolic", IconSize)
} }
@ -338,25 +324,11 @@ func (r *Row) SetSession(ses cchat.Session) {
// Set the states. // Set the states.
r.Session = ses r.Session = ses
r.sessionID = ses.ID() r.sessionID = ses.ID()
r.SetTooltipMarkup(markup.Render(ses.Name()))
r.avatar.SetText(ses.Name().Content)
if r.icon == nil {
r.icon = newIcon(r.avatar)
r.iconBox.Add(r.icon)
}
r.icon.SetPlaceholderIcon(IconName, IconSize) r.icon.SetPlaceholderIcon(IconName, IconSize)
r.name.QueueNamer(context.Background(), ses)
// If the session has an icon, then use it.
if iconer := ses.AsIconer(); iconer != nil {
r.icon.AsyncSetIconer(iconer, "failed to set session icon")
}
// Update to indicate that we're done. // Update to indicate that we're done.
primitives.RemoveChildren(r)
r.SetSensitive(true) r.SetSensitive(true)
r.Add(r.iconBox)
// Bind extra menu items before loading. These items won't be clickable // Bind extra menu items before loading. These items won't be clickable
// during loading. // during loading.
@ -368,7 +340,7 @@ func (r *Row) SetSession(ses cchat.Session) {
// 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.
if cmder := ses.AsCommander(); cmder != nil { if cmder := ses.AsCommander(); cmder != nil {
r.cmder = commander.NewBuffer(ses.Name().String(), cmder) r.cmder = commander.NewBuffer(&r.name, cmder)
// Show the command button if the session actually supports the // Show the command button if the session actually supports the
// commander. // commander.
r.ActionsMenu.AddAction("Command Prompt", r.ShowCommander) r.ActionsMenu.AddAction("Command Prompt", r.ShowCommander)

View File

@ -3,8 +3,6 @@ package service
import ( import (
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/singlestack"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/spinner"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session" "github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server" "github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
@ -36,10 +34,6 @@ type View struct {
Services *List Services *List
ServerView *gtk.ScrolledWindow ServerView *gtk.ScrolledWindow
ServerStack *singlestack.Stack
// Servers *session.Servers // nil by default; use .Servers
} }
func NewView(ctrller Controller) *View { func NewView(ctrller Controller) *View {
@ -52,18 +46,10 @@ func NewView(ctrller Controller) *View {
view.Header.AppMenuBindSize(view.Services) view.Header.AppMenuBindSize(view.Services)
view.Header.Show() view.Header.Show()
// Make a stack for the middle panel.
view.ServerStack = singlestack.NewStack()
view.ServerStack.SetSizeRequest(150, -1) // min width
view.ServerStack.SetTransitionDuration(50)
view.ServerStack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
view.ServerStack.SetHomogeneous(true)
view.ServerStack.Show()
primitives.AddClass(view.ServerStack, "server-stack")
view.ServerView, _ = gtk.ScrolledWindowNew(nil, nil) view.ServerView, _ = gtk.ScrolledWindowNew(nil, nil)
view.ServerView.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) view.ServerView.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
view.ServerView.Add(view.ServerStack) view.ServerView.SetHExpand(true)
view.ServerView.SetVExpand(true)
view.ServerView.Show() view.ServerView.Show()
view.BottomPane, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) view.BottomPane, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
@ -95,14 +81,8 @@ func (v *View) SessionSelected(svc *Service, srow *session.Row) {
} }
} }
// !!!: SHITTY HACK!!! primitives.RemoveChildren(v.ServerView)
// We can do this, as we're keeping all the server lists in memory by Go's v.ServerView.Add(srow.Servers)
// reference anyway. In fact, cchat REQUIRES us to do so.
if srow.Session != nil {
v.ServerStack.SetVisibleChild(srow.Servers)
} else {
v.ServerStack.SetVisibleChild(spinner.NewVisible())
}
v.Header.SetSessionMenu(srow) v.Header.SetSessionMenu(srow)
v.Header.SetBreadcrumber(srow) v.Header.SetBreadcrumber(srow)

51
internal/ui/style.css Normal file
View File

@ -0,0 +1,51 @@
/*
* Global CSS
*/
/* Make CSS more consistent across themes */
headerbar { padding-left: 0 }
/* .appmenu { margin: 0 20px } */
popover > *:not(stack):not(button) { margin: 6px }
/* Hack to fix the input bar being high in Adwaita */
.input-field * { min-height: 0 }
/* Hide all scroll undershoots */
undershoot { background-size: 0 }
/*
* Server CSS
*/
.top-level .server-list.expanded {
background-color: @borders;
}
.top-level .server-button {
border-radius: 0;
background-color: transparent;
}
/* .top-level .server-button:checked + revealer { */
/* border-left: 2px solid alpha(@borders, 0.75); */
/* } */
.top-level .server-children {
transition: margin-left 100ms;
}
.top-level.expand .server-button:checked + revealer > .server-children {
margin-left: 18px;
}
/* Keep this consistent with server.go's background-color. */
.top-level + stack .top-level {
background-color: alpha(@theme_selected_bg_color, 0.25);
}
/* Prevent inconsistent color when hovering over an active button. */
.server.active-column .server-button:focus {
box-shadow: none;
}

View File

@ -17,28 +17,20 @@ import (
"github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors" "github.com/pkg/errors"
_ "embed"
) )
//go:embed style.css
var styleCSS string
func init() { func init() {
// Load the local CSS. // Load the local CSS.
gts.LoadCSS("main", ` gts.LoadCSS("main", styleCSS)
/* Make CSS more consistent across themes */
headerbar { padding-left: 0 }
/* .appmenu { margin: 0 20px } */
popover > *:not(stack):not(button) { margin: 6px }
/* Hack to fix the input bar being high in Adwaita */
.input-field * { min-height: 0 }
/* Hide all scroll undershoots */
undershoot { background-size: 0 }
`)
} }
// constraints for the left panel // constraints for the left panel
const leftCurrentWidth = 300 const leftCurrentWidth = 350
func clamp(n, min, max int) int { func clamp(n, min, max int) int {
switch { switch {
@ -186,7 +178,7 @@ func (app *App) MessengerSelected(ses *session.Row, srv *server.ServerRow) {
app.lastSelector = srv.SetSelected app.lastSelector = srv.SetSelected
app.lastSelector(true) app.lastSelector(true)
app.MessageView.JoinServer(ses.Session, srv.Server, srv) app.MessageView.JoinServer(ses, srv, srv)
} }
// MessageView methods. // MessageView methods.
@ -207,8 +199,8 @@ func (app *App) OnMessageDone() {
} }
func (app *App) AuthenticateSession(list *service.List, ssvc *service.Service) { func (app *App) AuthenticateSession(list *service.List, ssvc *service.Service) {
var svc = ssvc.Service() svc := ssvc.Service()
auth.NewDialog(svc.Name(), svc.Authenticate(), func(ses cchat.Session) { auth.NewDialog(ssvc.Name.Label(), svc.Authenticate(), func(ses cchat.Session) {
ssvc.AddSession(ses) ssvc.AddSession(ses)
}) })
} }
@ -219,14 +211,12 @@ func (app *App) Close() {
// done, the application would exit immediately. There's no need to update // done, the application would exit immediately. There's no need to update
// the GUI. // the GUI.
for _, s := range app.Services.Services.Services { for _, s := range app.Services.Services.Services {
var service = s.Service().Name()
for _, session := range s.BodyList.Sessions() { for _, session := range s.BodyList.Sessions() {
if session.Session == nil { if session.Session == nil {
continue continue
} }
log.Printlnf("Disconnecting %s session %s", service, session.ID()) log.Printlnf("Disconnecting %s session %s", s.ID(), session.ID())
if err := session.Session.Disconnect(); err != nil { if err := session.Session.Disconnect(); err != nil {
log.Error(errors.Wrap(err, "Failed to disconnect "+session.ID())) log.Error(errors.Wrap(err, "Failed to disconnect "+session.ID()))

View File

@ -10,11 +10,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/config" "github.com/diamondburned/cchat-gtk/internal/ui/config"
"github.com/diamondburned/cchat/services" "github.com/diamondburned/cchat/services"
// _ "github.com/diamondburned/gotk3-tcmalloc"
// "github.com/diamondburned/gotk3-tcmalloc/heapprofiler"
_ "github.com/diamondburned/cchat-discord" _ "github.com/diamondburned/cchat-discord"
_ "github.com/diamondburned/cchat-mock"
) )
func init() { func init() {

15
profile.go Normal file
View File

@ -0,0 +1,15 @@
// Code generated by goprofiler. DO NOT EDIT.
package main
import (
"net/http"
_ "net/http/pprof"
)
func init() {
go func() {
println("Serving HTTP at 127.0.0.1:48574 for profiler at /debug/pprof")
panic(http.ListenAndServe("127.0.0.1:48574", nil))
}()
}

View File

@ -1,31 +1,11 @@
{ pkgs ? import <nixpkgs> {} }: { pkgs ? import <nixpkgs> {} }:
let nostrip = pkg: pkgs.enableDebugging (pkg.overrideAttrs(old: { pkgs.stdenv.mkDerivation rec {
dontStrip = true;
doCheck = false;
NIX_CFLAGS_COMPILE = (old.NIX_CFLAGS_COMPILE or "") + " -g";
}));
libhandy = pkgs.libhandy.overrideAttrs(old: {
name = "libhandy-1.0.1";
src = builtins.fetchGit {
url = "https://gitlab.gnome.org/GNOME/libhandy.git";
rev = "5cee0927b8b39dea1b2a62ec6d19169f73ba06c6";
};
patches = [];
buildInputs = old.buildInputs ++ (with pkgs; [
(nostrip gnome3.librsvg)
(nostrip gdk-pixbuf)
]);
});
in pkgs.stdenv.mkDerivation rec {
name = "cchat-gtk"; name = "cchat-gtk";
version = "0.0.2"; version = "0.0.2";
buildInputs = [ buildInputs = [
libhandy pkgs.libhandy
pkgs.gnome3.gspell pkgs.gnome3.gspell
pkgs.gnome3.glib pkgs.gnome3.glib
pkgs.gnome3.gtk pkgs.gnome3.gtk