Refactor server browser (message list not working)
This commit is contained in:
parent
b822f63c18
commit
e73f9a099b
18
go.mod
18
go.mod
|
@ -1,26 +1,20 @@
|
|||
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/gotk3-tcmalloc => ../../gotk3-tcmalloc
|
||||
// replace github.com/diamondburned/ningen/v2 => ../../ningen
|
||||
// replace github.com/diamondburned/arikawa/v2 => ../../arikawa
|
||||
replace github.com/diamondburned/cchat-discord => ../cchat-discord
|
||||
|
||||
require (
|
||||
github.com/Xuanwo/go-locale v1.0.0
|
||||
github.com/alecthomas/chroma v0.7.3
|
||||
github.com/diamondburned/cchat v0.3.17
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20210107014523-4fefdf1b9332
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db
|
||||
github.com/diamondburned/cchat v0.6.4
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff
|
||||
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/disintegration/imaging v1.6.2
|
||||
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/peterbourgon/diskv v2.0.1+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
|
|
101
go.sum
101
go.sum
|
@ -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.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
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/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/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/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/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=
|
||||
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=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
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/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg=
|
||||
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/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/kong v0.2.4 h1:Y0ZBCHAvHhTHw7FFJ2FzCAAG4pkbTgA45nc7BpMhDNk=
|
||||
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/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/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
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/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/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
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/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/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
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/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=
|
||||
|
@ -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-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.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/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/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/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/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
|
||||
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-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-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/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
|
||||
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-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-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/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
||||
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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
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/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/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/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-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/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
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/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-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
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.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.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg=
|
||||
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.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.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
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/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.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.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
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/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-20200212024743-f11f1df84d12 h1:TgXhFz35pKlZuUz1pNlOKk1UCSXPpuUIc144Wd7SxCA=
|
||||
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/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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
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/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
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/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.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
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/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/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=
|
||||
|
@ -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/kawasin73/umutex v0.2.1 h1:Onkzz3LKs1HThskVwdhhBocqdRQqwCZ03quDJzuPzPo=
|
||||
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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
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/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/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.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||
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/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/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
|
||||
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/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
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/yuin/goldmark v1.1.30 h1:j4d4Lw3zqZelDhBksEo3BnWg9xhXRQGJPPSL6OApZjI=
|
||||
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/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
|
||||
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.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=
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
|
||||
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-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-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
|
||||
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-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-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-20200207192155-f17229e696bd h1:zkO/Lhoka23X63N9OSzpSeROEUQ5ODw47tM3YWjygbs=
|
||||
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-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-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-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw=
|
||||
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-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
|
||||
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.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.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
||||
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-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-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-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
|
||||
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-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-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/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=
|
||||
|
@ -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-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-20200212150539-ea181f53ac56 h1:DFtSed2q3HtNuVazwVDZ4nSRS/JrZEig0gz2BY4VNrg=
|
||||
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-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=
|
||||
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=
|
||||
|
@ -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.14.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/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.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.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
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-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-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-20200212174721-66ed5ce911ce h1:1mbrb1tUU+Zmt5C94IGKADBTJZjZXAd+BubWi7r9EiI=
|
||||
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.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.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.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
|
||||
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/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/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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
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-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.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
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/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
|
||||
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=
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -3,6 +3,8 @@ package icons
|
|||
import (
|
||||
"log"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/gotk3/gotk3/cairo"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
)
|
||||
|
@ -10,6 +12,12 @@ import (
|
|||
// static assets
|
||||
// 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 {
|
||||
return mustSurface(loadPixbuf(__cchat_variant2_256, sz, scale), scale)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package httputil
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"mime"
|
||||
|
@ -9,7 +8,6 @@ import (
|
|||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
|
@ -21,32 +19,6 @@ import (
|
|||
"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 {
|
||||
primitives.Connector
|
||||
|
||||
|
@ -93,9 +65,19 @@ func AsyncImage(ctx context.Context,
|
|||
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() {
|
||||
// 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.
|
||||
mimeType := mime.TypeByExtension(urlExt(imageURL))
|
||||
|
||||
|
@ -143,20 +125,11 @@ func AsyncImage(ctx context.Context,
|
|||
l.Connect("area-prepared", load)
|
||||
l.Connect("area-updated", load)
|
||||
|
||||
// Borrow a buffered writer and return it at the end.
|
||||
bufWriter := bufferedWriter(l)
|
||||
defer returnBufferedWriter(bufWriter)
|
||||
|
||||
if err := downloadImage(r.Body, bufWriter, procs, isGIF); err != nil {
|
||||
if err := downloadImage(r.Body, l, procs, isGIF); err != nil {
|
||||
log.Error(errors.Wrapf(err, "failed to download %q", imageURL))
|
||||
// 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 {
|
||||
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()) {
|
||||
gts.ExecLater(func() {
|
||||
if ctx.Err() == nil {
|
||||
fn()
|
||||
}
|
||||
})
|
||||
if ctx.Err() == nil {
|
||||
gts.ExecAsync(func() {
|
||||
if ctx.Err() == nil {
|
||||
fn()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func downloadImage(src io.Reader, dst io.Writer, p []imgutil.Processor, isGIF bool) error {
|
||||
|
|
|
@ -17,16 +17,16 @@ var store = driver.NewStore(
|
|||
)
|
||||
|
||||
type Session struct {
|
||||
ID string
|
||||
ID cchat.ID
|
||||
|
||||
// Metadata.
|
||||
Name string
|
||||
Data map[string]string
|
||||
}
|
||||
|
||||
// ConvertSession attempts to get the session data from the given cchat session.
|
||||
// It returns nil if it can't do it.
|
||||
func ConvertSession(ses cchat.Session) *Session {
|
||||
var name = ses.Name().Content
|
||||
|
||||
func ConvertSession(ses cchat.Session, name string) *Session {
|
||||
saver := ses.AsSessionSaver()
|
||||
if saver == nil {
|
||||
return nil
|
||||
|
@ -45,28 +45,60 @@ func ConvertSession(ses cchat.Session) *Session {
|
|||
}
|
||||
}
|
||||
|
||||
func SaveSessions(service cchat.Service, sessions []Session) {
|
||||
if err := store.Set(service.Name().Content, sessions); err != nil {
|
||||
log.Warn(errors.Wrap(err, "Error saving session"))
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// RestoreSession restores a single session.
|
||||
func RestoreSession(svc cchat.Service, sessionID cchat.ID) *Session {
|
||||
service := Restore(svc)
|
||||
for _, session := range service.Sessions {
|
||||
if session.ID == sessionID {
|
||||
return &session
|
||||
}
|
||||
}
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to open file")
|
||||
return errors.Wrap(err, "failed to open file")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Write(v); err != nil {
|
||||
return errors.Wrap(err, "Failed to write")
|
||||
return errors.Wrap(err, "failed to write")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -72,14 +72,19 @@ func SaveToFile(file string, v []byte) error {
|
|||
func MarshalToFile(file string, from interface{}) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to open file")
|
||||
return errors.Wrap(err, "failed to open file")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
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
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -12,40 +12,31 @@ type Container struct {
|
|||
*container.ListContainer
|
||||
}
|
||||
|
||||
var _ container.Container = (*Container)(nil)
|
||||
|
||||
func NewContainer(ctrl container.Controller) *Container {
|
||||
c := container.NewListContainer(ctrl, constructors)
|
||||
c := container.NewListContainer(ctrl)
|
||||
primitives.AddClass(c, "compact-container")
|
||||
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) {
|
||||
gts.ExecAsync(func() {
|
||||
c.ListContainer.CreateMessageUnsafe(msg)
|
||||
c.ListContainer.CleanMessages()
|
||||
msg := WrapMessage(message.NewState(msg))
|
||||
c.ListContainer.AddMessage(msg)
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
gts.ExecAsync(func() { c.ListContainer.DeleteMessageUnsafe(msg) })
|
||||
}
|
||||
|
||||
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)
|
||||
gts.ExecAsync(func() { c.PopMessage(msg.ID()) })
|
||||
}
|
||||
|
|
|
@ -3,14 +3,13 @@ package compact
|
|||
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/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/rich/labeluri"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
)
|
||||
|
@ -29,41 +28,32 @@ var messageAuthorCSS = primitives.PrepareClassCSS("", `
|
|||
`)
|
||||
|
||||
type PresendMessage struct {
|
||||
message.PresendContainer
|
||||
message.Presender
|
||||
Message
|
||||
}
|
||||
|
||||
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
|
||||
msgc := message.NewPresendContainer(msg)
|
||||
|
||||
func WrapPresendMessage(pstate *message.PresendState) PresendMessage {
|
||||
return PresendMessage{
|
||||
PresendContainer: msgc,
|
||||
Message: wrapMessage(msgc.GenericContainer),
|
||||
Presender: pstate,
|
||||
Message: WrapMessage(pstate.State),
|
||||
}
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
*message.GenericContainer
|
||||
*message.State
|
||||
Timestamp *gtk.Label
|
||||
Username *labeluri.Label
|
||||
|
||||
unwrap func()
|
||||
}
|
||||
|
||||
var _ container.MessageRow = (*Message)(nil)
|
||||
|
||||
func NewMessage(msg cchat.MessageCreate) 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 {
|
||||
func WrapMessage(ct *message.State) Message {
|
||||
ts := message.NewTimestamp()
|
||||
ts.SetVAlign(gtk.ALIGN_START)
|
||||
ts.SetText(humanize.TimeAgo(ct.Time))
|
||||
ts.SetTooltipText(ct.Time.Format(time.Stamp))
|
||||
ts.Show()
|
||||
messageTimeCSS(ts)
|
||||
|
||||
|
@ -80,31 +70,37 @@ func wrapMessage(ct *message.GenericContainer) Message {
|
|||
ct.PackStart(ct.Content, true, true, 0)
|
||||
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{
|
||||
GenericContainer: ct,
|
||||
Timestamp: ts,
|
||||
Username: user,
|
||||
State: ct,
|
||||
Timestamp: ts,
|
||||
Username: user,
|
||||
unwrap: ct.Author.Name.OnUpdate(func() {
|
||||
user.SetLabel(ct.Author.Name.Label())
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// SetReferenceHighlighter sets the reference highlighter into the message.
|
||||
func (m Message) SetReferenceHighlighter(r labeluri.ReferenceHighlighter) {
|
||||
m.GenericContainer.SetReferenceHighlighter(r)
|
||||
m.State.SetReferenceHighlighter(r)
|
||||
m.Username.SetReferenceHighlighter(r)
|
||||
}
|
||||
|
||||
func (m Message) UpdateTimestamp(t time.Time) {
|
||||
m.GenericContainer.UpdateTimestamp(t)
|
||||
m.Timestamp.SetText(humanize.TimeAgo(t))
|
||||
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
|
||||
}
|
||||
|
||||
func (m Message) UpdateAuthor(author cchat.Author) {
|
||||
m.GenericContainer.UpdateAuthor(author)
|
||||
|
||||
cfg := markup.RenderConfig{}
|
||||
cfg.NoReferencing = true
|
||||
cfg.SetForegroundAnchor(m.ContentBodyStyle)
|
||||
|
||||
m.Username.SetOutput(markup.RenderCmplxWithConfig(author.Name(), cfg))
|
||||
func (m Message) Unwrap(revert bool) *message.State {
|
||||
if revert {
|
||||
m.unwrap()
|
||||
|
||||
primitives.RemoveChildren(m)
|
||||
m.SetClass("")
|
||||
}
|
||||
|
||||
return m.State
|
||||
}
|
||||
|
|
|
@ -2,11 +2,8 @@ package container
|
|||
|
||||
import (
|
||||
"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/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/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
@ -17,45 +14,43 @@ const BacklogLimit = 50
|
|||
|
||||
type MessageRow interface {
|
||||
message.Container
|
||||
// Attach should only be called once.
|
||||
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)
|
||||
GrabFocus()
|
||||
}
|
||||
|
||||
type PresendMessageRow interface {
|
||||
MessageRow
|
||||
message.PresendContainer
|
||||
message.Presender
|
||||
}
|
||||
|
||||
// Container is a generic messages container for children messages for children
|
||||
// packages.
|
||||
type Container interface {
|
||||
gtk.IWidget
|
||||
cchat.MessagesContainer
|
||||
|
||||
// Reset resets the message container to its original state.
|
||||
Reset()
|
||||
|
||||
// CreateMessageUnsafe creates a new message and returns the index that is
|
||||
// the location the message is added to.
|
||||
CreateMessageUnsafe(cchat.MessageCreate) MessageRow
|
||||
UpdateMessageUnsafe(cchat.MessageUpdate)
|
||||
DeleteMessageUnsafe(cchat.MessageDelete)
|
||||
// SetSelf sets the author for the current user.
|
||||
SetSelf(self *message.Author)
|
||||
|
||||
// NewPresendMessage creates and adds a presend message state into the list.
|
||||
NewPresendMessage(state *message.PresendState) PresendMessageRow
|
||||
|
||||
// AddMessage adds a new message into the list.
|
||||
AddMessage(row MessageRow)
|
||||
|
||||
// FirstMessage returns the first message in the buffer. Nil is returned if
|
||||
// there's nothing.
|
||||
FirstMessage() MessageRow
|
||||
// AddPresendMessage adds and displays an unsent message.
|
||||
AddPresendMessage(msg input.PresendMessage) PresendMessageRow
|
||||
// LatestMessageFrom returns the last message ID with that author.
|
||||
LatestMessageFrom(authorID string) (msgID string, ok bool)
|
||||
// Message finds and returns the message, if any.
|
||||
// LastMessage returns the last message in the buffer or nil if there's
|
||||
// nothing.
|
||||
LastMessage() MessageRow
|
||||
// Message finds and returns the message, if any. It performs maximum 2
|
||||
// constant-time lookups.
|
||||
Message(id cchat.ID, nonce string) MessageRow
|
||||
// FindMessage finds a message that satisfies the given callback.
|
||||
// FindMessage finds a message that satisfies the given callback. It
|
||||
// iterates the message buffer from latest to earliest.
|
||||
FindMessage(isMessage func(MessageRow) bool) MessageRow
|
||||
|
||||
// Highlight temporarily highlights the given message for a short while.
|
||||
|
@ -67,6 +62,21 @@ type Container interface {
|
|||
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.
|
||||
type Controller interface {
|
||||
// Connector is used for button press events to unselect messages.
|
||||
|
@ -77,20 +87,13 @@ type Controller interface {
|
|||
Bottomed() bool
|
||||
// AuthorEvent is called on message create/update. This is used to update
|
||||
// the typer state.
|
||||
AuthorEvent(a cchat.Author)
|
||||
AuthorEvent(authorID cchat.ID)
|
||||
// SelectMessage is called when a message is selected.
|
||||
SelectMessage(list *ListStore, msg MessageRow)
|
||||
// UnselectMessage is called when the message selection is cleared.
|
||||
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
|
||||
|
||||
// ListContainer is an implementation of Container, which allows flexible
|
||||
|
@ -106,7 +109,7 @@ type ListContainer struct {
|
|||
// messageRow w/ required internals
|
||||
type messageRow struct {
|
||||
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,
|
||||
|
@ -118,10 +121,8 @@ func unwrapRow(msg *messageRow) MessageRow {
|
|||
return msg.MessageRow
|
||||
}
|
||||
|
||||
var _ Container = (*ListContainer)(nil)
|
||||
|
||||
func NewListContainer(ctrl Controller, constr Constructor) *ListContainer {
|
||||
listStore := NewListStore(ctrl, constr)
|
||||
func NewListContainer(ctrl Controller) *ListContainer {
|
||||
listStore := NewListStore(ctrl)
|
||||
listStore.ListBox.Show()
|
||||
|
||||
clamp := handy.ClampNew()
|
||||
|
@ -139,12 +140,10 @@ func NewListContainer(ctrl Controller, constr Constructor) *ListContainer {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: remove useless abstraction (this file).
|
||||
|
||||
// // CreateMessageUnsafe inserts a message. It does not clean up old messages.
|
||||
// func (c *ListContainer) CreateMessageUnsafe(msg cchat.MessageCreate) MessageRow {
|
||||
// return c.ListStore.CreateMessageUnsafe(msg)
|
||||
// }
|
||||
func (c *ListContainer) AddMessage(row MessageRow) {
|
||||
c.ListStore.AddMessage(row)
|
||||
c.CleanMessages()
|
||||
}
|
||||
|
||||
// CleanMessages cleans up the oldest messages if the user is scrolled to the
|
||||
// bottom. True is returned if there were changes.
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -6,24 +6,10 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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
|
||||
// they're full or collapsed.
|
||||
type Collapsible interface {
|
||||
|
@ -43,29 +29,26 @@ const (
|
|||
AvatarMargin = 10
|
||||
)
|
||||
|
||||
var messageConstructors = container.Constructor{
|
||||
NewMessage: NewMessage,
|
||||
NewPresendMessage: NewPresendMessage,
|
||||
}
|
||||
|
||||
// NewMessage creates a new message.
|
||||
func NewMessage(
|
||||
msg cchat.MessageCreate, before container.MessageRow) container.MessageRow {
|
||||
s *message.State, before container.MessageRow) container.MessageRow {
|
||||
|
||||
if isCollapsible(before, msg) {
|
||||
return NewCollapsedMessage(msg)
|
||||
if isCollapsible(before, s) {
|
||||
return WrapCollapsedMessage(s)
|
||||
}
|
||||
|
||||
return NewFullMessage(msg)
|
||||
return WrapFullMessage(s)
|
||||
}
|
||||
|
||||
// NewPresendMessage creates a new presend message.
|
||||
func NewPresendMessage(
|
||||
msg input.PresendMessage, before container.MessageRow) container.PresendMessageRow {
|
||||
s *message.PresendState, before container.MessageRow) container.PresendMessageRow {
|
||||
|
||||
if isCollapsible(before, msg) {
|
||||
return NewCollapsedSendingMessage(msg)
|
||||
if isCollapsible(before, s.State) {
|
||||
return WrapCollapsedSendingMessage(s)
|
||||
}
|
||||
|
||||
return NewFullSendingMessage(msg)
|
||||
return WrapFullSendingMessage(s)
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
|
@ -73,7 +56,7 @@ type Container struct {
|
|||
}
|
||||
|
||||
func NewContainer(ctrl container.Controller) *Container {
|
||||
c := container.NewListContainer(ctrl, messageConstructors)
|
||||
c := container.NewListContainer(ctrl)
|
||||
primitives.AddClass(c, "cozy-container")
|
||||
return &Container{ListContainer: c}
|
||||
}
|
||||
|
@ -81,105 +64,72 @@ func NewContainer(ctrl container.Controller) *Container {
|
|||
func (c *Container) findAuthorID(authorID string) container.MessageRow {
|
||||
// Search the old author if we have any.
|
||||
return c.ListStore.FindMessage(func(msgc container.MessageRow) bool {
|
||||
return msgc.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
|
||||
|
||||
// isCollapsible returns true if the given lastMsg has matching conditions with
|
||||
// the given msg.
|
||||
func isCollapsible(lastMsg container.MessageRow, msg authoredMessage) bool {
|
||||
if lastMsg == nil || msg == nil {
|
||||
func isCollapsible(last container.MessageRow, msg *message.State) bool {
|
||||
if last == nil || msg.ID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
lastAuthor := lastMsg.Author()
|
||||
thisAuthor := msg.Author()
|
||||
lastMsg := last.Unwrap(false)
|
||||
|
||||
return true &&
|
||||
lastAuthor.ID() == thisAuthor.ID() &&
|
||||
lastAuthor.Name().String() == thisAuthor.Name().String() &&
|
||||
lastMsg.Time().Add(splitDuration).After(msg.Time())
|
||||
lastMsg.Author.ID == msg.ID &&
|
||||
lastMsg.Time.Add(splitDuration).After(msg.Time)
|
||||
}
|
||||
|
||||
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
|
||||
msgr := NewPresendMessage(state, c.LastMessage())
|
||||
c.AddMessage(msgr)
|
||||
return msgr
|
||||
}
|
||||
|
||||
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||
gts.ExecAsync(func() {
|
||||
// Create the message in the parent's handler. This handler will also
|
||||
// wipe old messages.
|
||||
row := c.ListContainer.CreateMessageUnsafe(msg)
|
||||
state := message.NewState(msg)
|
||||
msgr := NewMessage(state, c.LastMessage())
|
||||
|
||||
// Is this a full message? If so, then we should fetch the avatar when
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
c.AddMessage(msgr)
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
gts.ExecAsync(func() {
|
||||
c.UpdateMessageUnsafe(msg)
|
||||
})
|
||||
gts.ExecAsync(func() { container.UpdateMessage(c, msg) })
|
||||
}
|
||||
|
||||
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||
|
@ -202,10 +152,13 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
|||
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):
|
||||
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
|
||||
// message.
|
||||
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
|
||||
// author, then we don't need to uncollapse it.
|
||||
if next.Author().ID() != msgAuthorID {
|
||||
if nextHeader.Author.ID != msgHeader.Author.ID {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -223,33 +176,11 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
|||
}
|
||||
|
||||
func (c *Container) uncompact(msg container.MessageRow) {
|
||||
// We should only uncompact the message if it's compacted in the first
|
||||
// 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.
|
||||
full := WrapFullMessage(msg.Unwrap(true))
|
||||
c.ListStore.SwapMessage(full)
|
||||
}
|
||||
|
||||
func (c *Container) compact(msg container.MessageRow) {
|
||||
full, ok := msg.(*FullMessage)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
compact := WrapCollapsedMessage(full.Unwrap())
|
||||
message.RefreshContainer(compact, compact.GenericContainer)
|
||||
|
||||
compact := WrapCollapsedMessage(msg.Unwrap(true))
|
||||
c.ListStore.SwapMessage(compact)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
package cozy
|
||||
|
||||
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/input"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
@ -15,17 +10,12 @@ import (
|
|||
// the header, and the avatar is invisible.
|
||||
type CollapsedMessage struct {
|
||||
// Author is still updated normally.
|
||||
*message.GenericContainer
|
||||
*message.State
|
||||
Timestamp *gtk.Label
|
||||
}
|
||||
|
||||
func NewCollapsedMessage(msg cchat.MessageCreate) *CollapsedMessage {
|
||||
msgc := WrapCollapsedMessage(message.NewContainer(msg))
|
||||
message.FillContainer(msgc, msg)
|
||||
return msgc
|
||||
}
|
||||
|
||||
func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
|
||||
// WrapCollapsedMessage wraps the given message state to be a collapsed message.
|
||||
func WrapCollapsedMessage(gc *message.State) *CollapsedMessage {
|
||||
// Set Timestamp's padding accordingly to Avatar's.
|
||||
ts := message.NewTimestamp()
|
||||
ts.SetSizeRequest(AvatarSize, -1)
|
||||
|
@ -42,37 +32,31 @@ func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
|
|||
gc.SetClass("cozy-collapsed")
|
||||
|
||||
return &CollapsedMessage{
|
||||
GenericContainer: gc,
|
||||
Timestamp: ts,
|
||||
State: gc,
|
||||
Timestamp: ts,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CollapsedMessage) Collapsed() bool { return true }
|
||||
|
||||
func (c *CollapsedMessage) UpdateTimestamp(t time.Time) {
|
||||
c.GenericContainer.UpdateTimestamp(t)
|
||||
c.Timestamp.SetText(humanize.TimeAgoShort(t))
|
||||
}
|
||||
func (c *CollapsedMessage) Unwrap(revert bool) *message.State {
|
||||
if revert {
|
||||
// Remove State's widgets from the containers.
|
||||
c.Remove(c.Timestamp)
|
||||
c.Remove(c.Content)
|
||||
}
|
||||
|
||||
func (c *CollapsedMessage) Unwrap() *message.GenericContainer {
|
||||
// Remove GenericContainer's widgets from the containers.
|
||||
c.Remove(c.Timestamp)
|
||||
c.Remove(c.Content)
|
||||
|
||||
// Return after removing.
|
||||
return c.GenericContainer
|
||||
return c.State
|
||||
}
|
||||
|
||||
type CollapsedSendingMessage struct {
|
||||
*CollapsedMessage
|
||||
message.PresendContainer
|
||||
message.Presender
|
||||
}
|
||||
|
||||
func NewCollapsedSendingMessage(msg input.PresendMessage) *CollapsedSendingMessage {
|
||||
var msgc = message.NewPresendContainer(msg)
|
||||
|
||||
func WrapCollapsedSendingMessage(pstate *message.PresendState) *CollapsedSendingMessage {
|
||||
return &CollapsedSendingMessage{
|
||||
CollapsedMessage: WrapCollapsedMessage(msgc.GenericContainer),
|
||||
PresendContainer: msgc,
|
||||
CollapsedMessage: WrapCollapsedMessage(pstate.State),
|
||||
Presender: pstate,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,13 @@ import (
|
|||
"time"
|
||||
|
||||
"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/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/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/parser/markup"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/cairo"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
|
@ -23,22 +18,20 @@ import (
|
|||
const TopFullMargin = 4
|
||||
|
||||
type FullMessage struct {
|
||||
*message.GenericContainer
|
||||
*message.State
|
||||
|
||||
// Grid widgets.
|
||||
Avatar *Avatar
|
||||
MainBox *gtk.Box // wraps header and content
|
||||
|
||||
Header *labeluri.Label
|
||||
timestamp string // markup
|
||||
}
|
||||
HeaderLabel *labeluri.Label
|
||||
timestamp string // markup
|
||||
|
||||
type AvatarPixbufCopier interface {
|
||||
CopyAvatarPixbuf(img httputil.SurfaceContainer) bool
|
||||
// unwrap is used to removing label handlers.
|
||||
unwrap func()
|
||||
}
|
||||
|
||||
var (
|
||||
_ AvatarPixbufCopier = (*FullMessage)(nil)
|
||||
_ message.Container = (*FullMessage)(nil)
|
||||
_ container.MessageRow = (*FullMessage)(nil)
|
||||
)
|
||||
|
@ -51,22 +44,16 @@ var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", `
|
|||
`)
|
||||
|
||||
func NewFullMessage(msg cchat.MessageCreate) *FullMessage {
|
||||
msgc := WrapFullMessage(message.NewContainer(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
|
||||
return WrapFullMessage(message.NewState(msg))
|
||||
}
|
||||
|
||||
func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
||||
func WrapFullMessage(gc *message.State) *FullMessage {
|
||||
header := labeluri.NewLabel(text.Rich{})
|
||||
header.SetHAlign(gtk.ALIGN_START) // left-align
|
||||
header.SetMaxWidthChars(100)
|
||||
header.Show()
|
||||
|
||||
avatar := NewAvatar()
|
||||
avatar := NewAvatar(gc.Row)
|
||||
avatar.SetMarginTop(TopFullMargin / 2)
|
||||
avatar.SetMarginStart(container.ColumnSpacing * 2)
|
||||
avatar.Connect("clicked", func(w gtk.IWidget) {
|
||||
|
@ -97,87 +84,61 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
|||
gc.PackStart(main, true, true, 0)
|
||||
gc.SetClass("cozy-full")
|
||||
|
||||
return &FullMessage{
|
||||
GenericContainer: gc,
|
||||
msg := &FullMessage{
|
||||
State: gc,
|
||||
timestamp: formatLongTime(gc.Time),
|
||||
|
||||
Avatar: avatar,
|
||||
MainBox: main,
|
||||
Header: header,
|
||||
Avatar: avatar,
|
||||
MainBox: main,
|
||||
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) Unwrap() *message.GenericContainer {
|
||||
// Remove GenericContainer's widgets from the containers.
|
||||
m.Header.Destroy()
|
||||
m.MainBox.Remove(m.Content) // not ours, so don't destroy.
|
||||
func (m *FullMessage) Unwrap(revert bool) *message.State {
|
||||
if revert {
|
||||
// Remove the handlers.
|
||||
m.unwrap()
|
||||
|
||||
// Remove the message from the grid.
|
||||
m.Avatar.Destroy()
|
||||
m.MainBox.Destroy()
|
||||
// Remove State's widgets from the containers.
|
||||
m.HeaderLabel.Destroy()
|
||||
m.MainBox.Remove(m.Content) // not ours, so don't destroy.
|
||||
|
||||
// Return after removing.
|
||||
return m.GenericContainer
|
||||
}
|
||||
|
||||
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
|
||||
// Remove the message from the grid.
|
||||
m.Avatar.Destroy()
|
||||
m.MainBox.Destroy()
|
||||
}
|
||||
return true
|
||||
|
||||
return m.State
|
||||
}
|
||||
|
||||
func (m *FullMessage) AttachMenu(items []menu.Item) {
|
||||
// Bind to parent's container as well.
|
||||
m.GenericContainer.AttachMenu(items)
|
||||
|
||||
// Bind to the box.
|
||||
// TODO lol
|
||||
func formatLongTime(t time.Time) string {
|
||||
return `<span alpha="70%" size="small">` + humanize.TimeAgoLong(t) + `</span>`
|
||||
}
|
||||
|
||||
type FullSendingMessage struct {
|
||||
message.PresendContainer
|
||||
FullMessage
|
||||
*FullMessage
|
||||
message.Presender
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -185,51 +146,9 @@ var (
|
|||
_ container.MessageRow = (*FullSendingMessage)(nil)
|
||||
)
|
||||
|
||||
func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage {
|
||||
var msgc = message.NewPresendContainer(msg)
|
||||
|
||||
func WrapFullSendingMessage(pstate *message.PresendState) *FullSendingMessage {
|
||||
return &FullSendingMessage{
|
||||
PresendContainer: msgc,
|
||||
FullMessage: *WrapFullMessage(msgc.GenericContainer),
|
||||
FullMessage: WrapFullMessage(pstate.State),
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"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/rich/parser/markup"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
|
@ -63,18 +63,19 @@ var messageListCSS = primitives.PrepareClassCSS("message-list", `
|
|||
.message-list { background: transparent; }
|
||||
`)
|
||||
|
||||
type ListStore struct {
|
||||
ListBox *gtk.ListBox
|
||||
var fallbackAuthor = message.NewCustomAuthor("", text.Plain("self"))
|
||||
|
||||
Construct Constructor
|
||||
type ListStore struct {
|
||||
ListBox *gtk.ListBox
|
||||
Controller Controller
|
||||
|
||||
resetMe bool
|
||||
self *message.Author
|
||||
|
||||
resetMe bool
|
||||
messages map[messageKey]*messageRow
|
||||
}
|
||||
|
||||
func NewListStore(ctrl Controller, constr Constructor) *ListStore {
|
||||
func NewListStore(ctrl Controller) *ListStore {
|
||||
listBox, _ := gtk.ListBoxNew()
|
||||
listBox.SetSelectionMode(gtk.SELECTION_SINGLE)
|
||||
listBox.Show()
|
||||
|
@ -82,9 +83,9 @@ func NewListStore(ctrl Controller, constr Constructor) *ListStore {
|
|||
|
||||
listStore := ListStore{
|
||||
ListBox: listBox,
|
||||
Construct: constr,
|
||||
Controller: ctrl,
|
||||
messages: make(map[messageKey]*messageRow, BacklogLimit+1),
|
||||
self: &fallbackAuthor,
|
||||
}
|
||||
|
||||
var selected bool
|
||||
|
@ -113,9 +114,29 @@ func NewListStore(ctrl Controller, constr Constructor) *ListStore {
|
|||
return &listStore
|
||||
}
|
||||
|
||||
// Reset resets the list store.
|
||||
func (c *ListStore) Reset() {
|
||||
for _, msg := range c.messages {
|
||||
destroyMsg(msg)
|
||||
}
|
||||
|
||||
// Delegate removing children to the constructor.
|
||||
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 {
|
||||
|
@ -133,22 +154,27 @@ func (c *ListStore) SwapMessage(msg MessageRow) bool {
|
|||
msg = m.MessageRow
|
||||
}
|
||||
|
||||
msgState := msg.Unwrap(false)
|
||||
|
||||
// Get the current message's index.
|
||||
oldMsg, ix := c.findIndex(msg.ID())
|
||||
oldMsg, ix := c.findIndex(msgState.ID)
|
||||
if ix == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
oldState := oldMsg.Unwrap(false)
|
||||
|
||||
// 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
|
||||
// downwards.
|
||||
c.ListBox.Insert(msg.Row(), ix)
|
||||
c.ListBox.Insert(msgState.Row, ix)
|
||||
|
||||
// Set the message into the map.
|
||||
row := c.messages[idKey(msg.ID())]
|
||||
row := c.messages[idKey(msgState.ID)]
|
||||
row.MessageRow = msg
|
||||
c.bindMessage(row)
|
||||
|
||||
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) {
|
||||
c.ensureEmpty()
|
||||
|
||||
var last *messageRow
|
||||
var next bool
|
||||
|
||||
|
@ -192,25 +216,8 @@ func (c *ListStore) around(aroundID cchat.ID) (before, after *messageRow) {
|
|||
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.
|
||||
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
|
||||
// match, so the worst case is a single string hash.
|
||||
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) {
|
||||
c.ensureEmpty()
|
||||
|
||||
var r *messageRow
|
||||
var i = c.MessagesLen() - 1
|
||||
|
||||
|
@ -316,6 +321,9 @@ func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
|
|||
if nonce != "" {
|
||||
// Things in this map are guaranteed to have presend != nil.
|
||||
m, ok := c.messages[nonceKey(nonce)]
|
||||
|
||||
// This is honestly pretty dumb, but whatever.
|
||||
// TODO: make message() getter not set.
|
||||
if ok {
|
||||
// Replace the nonce key with ID.
|
||||
delete(c.messages, nonceKey(nonce))
|
||||
|
@ -334,135 +342,69 @@ func (c *ListStore) message(msgID cchat.ID, nonce string) *messageRow {
|
|||
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) {
|
||||
state := msgc.Unwrap(false)
|
||||
|
||||
// Bind the message ID to the row so we can easily do a lookup.
|
||||
var key messageKey
|
||||
if id := msgc.ID(); id != "" {
|
||||
key.id = id
|
||||
} else {
|
||||
key.id = msgc.Nonce()
|
||||
key := messageKey{
|
||||
id: state.ID,
|
||||
}
|
||||
|
||||
if state.Nonce != "" {
|
||||
key.id = state.Nonce
|
||||
key.nonce = true
|
||||
}
|
||||
|
||||
msgc.Row().SetName(key.name())
|
||||
msgc.SetReferenceHighlighter(c)
|
||||
state.Row.SetName(key.name())
|
||||
msgc.MessageRow.SetReferenceHighlighter(c)
|
||||
|
||||
c.Controller.BindMenu(msgc.MessageRow)
|
||||
}
|
||||
|
||||
// AddPresendMessage inserts an input.PresendMessage into the container and
|
||||
// returning a wrapped widget interface.
|
||||
func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRow {
|
||||
c.ensureEmpty()
|
||||
func (c *ListStore) AddMessage(msg MessageRow) {
|
||||
state := msg.Unwrap(false)
|
||||
|
||||
before := c.LastMessage()
|
||||
|
||||
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())
|
||||
defer c.Controller.AuthorEvent(state.Author.ID)
|
||||
|
||||
// Do not attempt to update before insertion (aka upsert).
|
||||
if msgc := c.message(msg.ID(), msg.Nonce()); msgc != nil {
|
||||
msgc.UpdateAuthor(msg.Author())
|
||||
msgc.UpdateContent(msg.Content(), false)
|
||||
msgc.UpdateTimestamp(msg.Time())
|
||||
|
||||
c.bindMessage(msgc)
|
||||
return msgc.MessageRow
|
||||
if msgc := c.message(state.ID, state.Nonce); msgc != nil {
|
||||
// This is kind of expensive, but it shouldn't really matter.
|
||||
c.SwapMessage(msg)
|
||||
return
|
||||
}
|
||||
|
||||
c.ensureEmpty()
|
||||
|
||||
msgTime := msg.Time()
|
||||
|
||||
// Iterate and compare timestamp to find where to insert a message. Note
|
||||
// that "before" is the message that will go before the to-be-inserted
|
||||
// method.
|
||||
before, index := c.findMessage(true, func(before *messageRow) bool {
|
||||
return msgTime.After(before.Time())
|
||||
return before.Unwrap(false).Time.After(state.Time)
|
||||
})
|
||||
|
||||
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
|
||||
// earliest message, therefore we prepend it.
|
||||
if before == nil {
|
||||
index = 0
|
||||
c.ListBox.Prepend(msgc.Row())
|
||||
c.ListBox.Prepend(state.Row)
|
||||
} else {
|
||||
index++ // insert right after
|
||||
|
||||
// Fast path: Insert did appear a lot on profiles, so we can try and use
|
||||
// Add over Insert when we know.
|
||||
if c.MessagesLen() == index {
|
||||
c.ListBox.Add(msgc.Row())
|
||||
c.ListBox.Add(state.Row)
|
||||
} else {
|
||||
c.ListBox.Insert(msgc.Row(), index)
|
||||
c.ListBox.Insert(state.Row, index)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the ID into the message map.
|
||||
c.messages[idKey(msgc.ID())] = msgc
|
||||
c.messages[idKey(state.ID)] = 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.
|
||||
|
@ -475,7 +417,8 @@ func (c *ListStore) PopMessage(id cchat.ID) (msg MessageRow) {
|
|||
msg = gridMsg.MessageRow
|
||||
|
||||
// Remove off of the Gtk grid.
|
||||
gridMsg.Row().Destroy()
|
||||
destroyMsg(gridMsg)
|
||||
|
||||
// Delete off the map.
|
||||
delete(c.messages, idKey(id))
|
||||
|
||||
|
@ -489,22 +432,23 @@ func (c *ListStore) DeleteEarliest(n int) {
|
|||
return
|
||||
}
|
||||
|
||||
c.ensureEmpty()
|
||||
|
||||
// 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.
|
||||
primitives.ForeachChild(c.ListBox, func(v interface{}) (stop bool) {
|
||||
id := parseKeyFromNamer(v.(primitives.Namer))
|
||||
gridMsg := c.message(id.expand())
|
||||
|
||||
if id := gridMsg.ID(); id != "" {
|
||||
delete(c.messages, idKey(id))
|
||||
}
|
||||
if nonce := gridMsg.Nonce(); nonce != "" {
|
||||
delete(c.messages, nonceKey(nonce))
|
||||
state := gridMsg.Unwrap(false)
|
||||
|
||||
if state.ID != "" {
|
||||
delete(c.messages, idKey(state.ID))
|
||||
}
|
||||
|
||||
gridMsg.Row().Destroy()
|
||||
if state.Nonce != "" {
|
||||
delete(c.messages, nonceKey(state.Nonce))
|
||||
}
|
||||
|
||||
destroyMsg(gridMsg)
|
||||
|
||||
n--
|
||||
return n == 0
|
||||
|
@ -519,10 +463,16 @@ func (c *ListStore) HighlightReference(ref markup.ReferenceSegment) {
|
|||
}
|
||||
|
||||
func (c *ListStore) Highlight(msg MessageRow) {
|
||||
gts.ExecLater(func() {
|
||||
row := msg.Row()
|
||||
row.GrabFocus()
|
||||
c.ListBox.DragHighlightRow(row)
|
||||
gts.ExecAsync(func() {
|
||||
state := msg.Unwrap(false)
|
||||
state.Row.GrabFocus()
|
||||
c.ListBox.DragHighlightRow(state.Row)
|
||||
gts.DoAfter(2*time.Second, c.ListBox.DragUnhighlightRow)
|
||||
})
|
||||
}
|
||||
|
||||
func destroyMsg(row *messageRow) {
|
||||
state := row.Unwrap(true)
|
||||
state.Author.Name.Stop()
|
||||
state.Row.Destroy()
|
||||
}
|
||||
|
|
|
@ -272,8 +272,8 @@ var deleteAttBtnCSS = primitives.PrepareCSS(`
|
|||
|
||||
func (c *Container) addPreview(name string, thumbnail *cairo.Surface) {
|
||||
// Make a fallback image first.
|
||||
gimg, _ := roundimage.NewImage(4) // border-radius: 4px
|
||||
primitives.SetImageIcon(gimg.Image, iconFromName(name), IconSize)
|
||||
gimg := roundimage.NewImage(4) // border-radius: 4px
|
||||
primitives.SetImageIcon(&gimg.Image, iconFromName(name), IconSize)
|
||||
gimg.SetSizeRequest(ThumbSize, ThumbSize)
|
||||
gimg.SetVAlign(gtk.ALIGN_CENTER)
|
||||
gimg.SetHAlign(gtk.ALIGN_CENTER)
|
||||
|
|
|
@ -6,11 +6,14 @@ import (
|
|||
"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/messages/container"
|
||||
"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/message"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||
"github.com/diamondburned/handy"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
|
@ -19,10 +22,12 @@ import (
|
|||
|
||||
// Controller is an interface to control message containers.
|
||||
type Controller interface {
|
||||
AddPresendMessage(msg PresendMessage) (onErr func(error))
|
||||
LatestMessageFrom(userID cchat.ID) (messageID cchat.ID, ok bool)
|
||||
MessageAuthor(msgID cchat.ID) cchat.Author
|
||||
Author(authorID cchat.ID) cchat.Author
|
||||
LatestMessageFrom(userID cchat.ID) container.MessageRow
|
||||
MessageAuthor(msgID cchat.ID) *message.Author
|
||||
Author(authorID cchat.ID) (name rich.LabelStateStorer)
|
||||
|
||||
// SendMessage asynchronously sends the given message.
|
||||
SendMessage(msg message.PresendMessage)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
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
|
||||
// with the MessageReference.
|
||||
name := author.Name()
|
||||
|
||||
for _, seg := range name.Segments {
|
||||
for _, seg := range label.Segments {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
id, ok := f.ctrl.LatestMessageFrom(f.UserID)
|
||||
if !ok {
|
||||
msgr := f.ctrl.LatestMessageFrom(f.UserID)
|
||||
if msgr == nil {
|
||||
// No messages found, so we can passthrough normally.
|
||||
return false
|
||||
}
|
||||
|
||||
id := msgr.Unwrap(false).ID
|
||||
|
||||
// If we don't support message editing, then passthrough events.
|
||||
if !f.Editable(id) {
|
||||
return false
|
||||
|
|
|
@ -8,10 +8,9 @@ import (
|
|||
"time"
|
||||
|
||||
"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/messages/input/attachment"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/twmb/murmur3"
|
||||
)
|
||||
|
@ -63,17 +62,9 @@ func (f *Field) sendInput() {
|
|||
return
|
||||
}
|
||||
|
||||
// Derive the author. Prefer the author of the current user from the message
|
||||
// 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{
|
||||
f.ctrl.SendMessage(SendMessageData{
|
||||
time: time.Now().UTC(),
|
||||
content: text,
|
||||
author: author,
|
||||
nonce: f.generateNonce(),
|
||||
replyID: f.replyingID,
|
||||
files: attachments,
|
||||
|
@ -86,19 +77,23 @@ func (f *Field) sendInput() {
|
|||
f.text.GrabFocus()
|
||||
}
|
||||
|
||||
func (f *Field) SendMessage(data PresendMessage) {
|
||||
// presend message into the container through the controller
|
||||
var onErr = f.ctrl.AddPresendMessage(data)
|
||||
// func (f *Field) SendMessage(data message.PresendMessage) {
|
||||
// f.ctrl.SendMessage(data)
|
||||
|
||||
// Copy the sender to prevent race conditions.
|
||||
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
|
||||
})
|
||||
}
|
||||
// // message.NewPresendState(f.Username.State, data)
|
||||
|
||||
// // // presend message into the container through the controller
|
||||
// // var onErr = f.ctrl.AddPresendMessage(data)
|
||||
|
||||
// // // Copy the sender to prevent race conditions.
|
||||
// // 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.
|
||||
type Files []attachment.File
|
||||
|
@ -117,56 +112,23 @@ func (files Files) Attachments() []cchat.MessageAttachment {
|
|||
type SendMessageData struct {
|
||||
time time.Time
|
||||
content string
|
||||
author cchat.Author
|
||||
nonce string
|
||||
replyID cchat.ID
|
||||
files Files
|
||||
}
|
||||
|
||||
var _ cchat.SendableMessage = (*SendMessageData)(nil)
|
||||
|
||||
// PresendMessage is an interface for any message about to be sent.
|
||||
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)
|
||||
var (
|
||||
_ cchat.SendableMessage = (*SendMessageData)(nil)
|
||||
_ message.PresendMessage = (*SendMessageData)(nil)
|
||||
)
|
||||
|
||||
// ID returns a pseudo ID for internal use.
|
||||
func (s SendMessageData) ID() string { return s.nonce }
|
||||
func (s SendMessageData) Time() time.Time { return s.time }
|
||||
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) Nonce() string { return s.nonce }
|
||||
func (s SendMessageData) Files() []attachment.File { return s.files }
|
||||
func (s SendMessageData) AsAttacher() cchat.Attacher { return s.files }
|
||||
func (s SendMessageData) AsReplier() cchat.Replier { return s }
|
||||
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 }
|
||||
|
|
|
@ -2,12 +2,14 @@ package username
|
|||
|
||||
import (
|
||||
"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/messages/message"
|
||||
"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/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
)
|
||||
|
||||
const AvatarSize = 24
|
||||
|
@ -24,21 +26,20 @@ func init() {
|
|||
}
|
||||
|
||||
type Container struct {
|
||||
*gtk.Revealer
|
||||
gtk.Revealer
|
||||
State *message.Author
|
||||
|
||||
main *gtk.Box
|
||||
avatar *rich.Icon
|
||||
avatar *roundimage.Image
|
||||
label *rich.Label
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.LabelContainer = (*Container)(nil)
|
||||
_ cchat.IconContainer = (*Container)(nil)
|
||||
)
|
||||
|
||||
var usernameCSS = primitives.PrepareCSS(`
|
||||
.username-view { margin: 0 5px }
|
||||
`)
|
||||
|
||||
var fallbackAuthor = message.NewCustomAuthor("", text.Plain("self"))
|
||||
|
||||
func NewContainer() *Container {
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
|
||||
box.Show()
|
||||
|
@ -56,13 +57,32 @@ func NewContainer() *Container {
|
|||
// thread.
|
||||
currentRevealer = rev.SetRevealChild
|
||||
|
||||
container := Container{
|
||||
Revealer: rev,
|
||||
author := message.NewCustomAuthor("", text.Plain("self"))
|
||||
|
||||
u := Container{
|
||||
Revealer: *rev,
|
||||
State: &author,
|
||||
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) {
|
||||
|
@ -72,75 +92,39 @@ func (u *Container) SetRevealChild(reveal bool) {
|
|||
|
||||
// shouldReveal returns whether or not the container should reveal.
|
||||
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() {
|
||||
u.SetRevealChild(false)
|
||||
|
||||
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)
|
||||
u.State.Name.Stop()
|
||||
}
|
||||
|
||||
// Update is not thread-safe.
|
||||
func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) {
|
||||
// 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.
|
||||
u.SetRevealChild(true)
|
||||
|
||||
// Does messenger implement Nicknamer? If yes, use it.
|
||||
if nicknamer := messenger.AsNicknamer(); nicknamer != nil {
|
||||
u.label.AsyncSetLabel(nicknamer.Nickname, "Error fetching server nickname")
|
||||
}
|
||||
|
||||
// Does session implement an icon? Update if yes.
|
||||
if iconer := session.AsIconer(); iconer != nil {
|
||||
u.avatar.AsyncSetIconer(iconer, "Error fetching session icon URL")
|
||||
u.State.Name.BindNamer(u.main, "destroy", nicknamer)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLabel is not thread-safe.
|
||||
func (u *Container) GetLabel() text.Rich {
|
||||
return u.label.GetLabel()
|
||||
// Label returns the underlying label.
|
||||
func (u *Container) Label() text.Rich {
|
||||
return u.State.Name.Label()
|
||||
}
|
||||
|
||||
// GetLabelMarkup is not thread-safe.
|
||||
// LabelMarkup returns the underlying label's markup.
|
||||
func (u *Container) GetLabelMarkup() string {
|
||||
return u.label.Label.GetLabel()
|
||||
}
|
||||
|
||||
// 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()
|
||||
return u.label.Output().Markup
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package memberlist
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"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.
|
||||
primitives.RemoveChildren(c.Main)
|
||||
|
||||
var newSections = make([]*Section, len(sections))
|
||||
newSections := make([]*Section, len(sections))
|
||||
oldSections := c.Sections
|
||||
|
||||
for i, section := range sections {
|
||||
sc, ok := c.Sections[section.ID()]
|
||||
if !ok {
|
||||
sc = NewSection(section, &c.eventQueue)
|
||||
} else {
|
||||
sc.Update(section.Name(), section.Total())
|
||||
sc.Update(section)
|
||||
}
|
||||
|
||||
newSections[i] = sc
|
||||
}
|
||||
|
||||
// Remove all old sections.
|
||||
|
||||
for id := range c.Sections {
|
||||
delete(c.Sections, id)
|
||||
}
|
||||
|
@ -152,6 +153,16 @@ func (c *Container) SetSectionsUnsafe(sections []cchat.MemberSection) {
|
|||
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)
|
||||
}
|
||||
|
||||
|
@ -173,7 +184,7 @@ type Section struct {
|
|||
ID string
|
||||
|
||||
// state
|
||||
name text.Rich
|
||||
name rich.NameContainer
|
||||
total int
|
||||
|
||||
Header *rich.Label
|
||||
|
@ -197,26 +208,40 @@ var sectionBodyCSS = primitives.PrepareClassCSS("section-body", `
|
|||
`)
|
||||
|
||||
func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
|
||||
header := rich.NewLabel(text.Rich{})
|
||||
header.Show()
|
||||
sectionHeaderCSS(header)
|
||||
section := &Section{
|
||||
ID: sect.ID(),
|
||||
name: rich.NameContainer{},
|
||||
}
|
||||
|
||||
body, _ := gtk.ListBoxNew()
|
||||
body.SetSelectionMode(gtk.SELECTION_NONE)
|
||||
body.SetActivateOnSingleClick(true)
|
||||
body.SetSortFunc(listSortNameAsc) // A-Z
|
||||
body.Show()
|
||||
sectionBodyCSS(body)
|
||||
section.Header = rich.NewLabel(§ion.name)
|
||||
section.Header.Show()
|
||||
sectionHeaderCSS(section.Header)
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.PackStart(header, false, false, 0)
|
||||
box.PackStart(body, false, false, 0)
|
||||
box.Show()
|
||||
section.Header.SetRenderer(func(rich text.Rich) markup.RenderOutput {
|
||||
out := markup.RenderCmplx(rich)
|
||||
if section.total > 0 {
|
||||
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{}
|
||||
|
||||
// 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()
|
||||
// Cold path; we can afford searching in the map.
|
||||
for _, member := range members {
|
||||
|
@ -226,32 +251,20 @@ func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section {
|
|||
}
|
||||
})
|
||||
|
||||
section := &Section{
|
||||
ID: sect.ID(),
|
||||
Box: box,
|
||||
Header: header,
|
||||
Body: body,
|
||||
Members: members,
|
||||
}
|
||||
|
||||
section.Update(sect.Name(), sect.Total())
|
||||
section.name.QueueNamer(context.Background(), sect)
|
||||
section.Header.Connect("destroy", section.name.Stop)
|
||||
|
||||
return section
|
||||
}
|
||||
|
||||
func (s *Section) Update(name text.Rich, total int) {
|
||||
s.name = name
|
||||
s.total = total
|
||||
func (s *Section) Destroy() {
|
||||
s.name.Stop()
|
||||
s.Box.Destroy()
|
||||
}
|
||||
|
||||
var content = s.name.Content
|
||||
if total > 0 {
|
||||
content += fmt.Sprintf("—%d", total)
|
||||
}
|
||||
|
||||
s.Header.SetLabelUnsafe(text.Rich{
|
||||
Content: content,
|
||||
Segments: s.name.Segments,
|
||||
})
|
||||
func (s *Section) Update(sect cchat.MemberSection) {
|
||||
s.total = sect.Total()
|
||||
s.name.QueueNamer(context.Background(), sect)
|
||||
}
|
||||
|
||||
func (s *Section) SetMember(member cchat.ListMember) {
|
||||
|
@ -292,14 +305,16 @@ type Member struct {
|
|||
*gtk.ListBoxRow
|
||||
Main *gtk.Box
|
||||
|
||||
Avatar *rich.Icon
|
||||
Name *gtk.Label
|
||||
output markup.RenderOutput
|
||||
Avatar *roundimage.StillImage
|
||||
Name *rich.Label
|
||||
|
||||
name rich.LabelState
|
||||
second text.Rich
|
||||
status cchat.Status
|
||||
parent *gtk.ListBox
|
||||
}
|
||||
|
||||
const AvatarSize = 34
|
||||
const AvatarSize = 32
|
||||
|
||||
var memberRowCSS = primitives.PrepareClassCSS("member-row", `
|
||||
.member-row {
|
||||
|
@ -320,46 +335,61 @@ var avatarMemberCSS = primitives.PrepareClassCSS("avatar-member", `
|
|||
`)
|
||||
|
||||
func NewMember(member cchat.ListMember) *Member {
|
||||
m := Member{}
|
||||
|
||||
evb, _ := gtk.EventBoxNew()
|
||||
evb.AddEvents(int(gdk.EVENT_ENTER_NOTIFY) | int(gdk.EVENT_LEAVE_NOTIFY))
|
||||
evb.Show()
|
||||
|
||||
img, _ := roundimage.NewStaticImage(evb, 0)
|
||||
img.Show()
|
||||
m.Avatar = roundimage.NewStillImage(evb, 9999)
|
||||
m.Avatar.SetSize(AvatarSize)
|
||||
m.Avatar.SetPlaceholderIcon("user-info-symbolic", AvatarSize)
|
||||
m.Avatar.Show()
|
||||
avatarMemberCSS(m.Avatar)
|
||||
|
||||
icon := rich.NewCustomIcon(img, AvatarSize)
|
||||
icon.SetPlaceholderIcon("user-info-symbolic", AvatarSize)
|
||||
icon.Show()
|
||||
avatarMemberCSS(icon)
|
||||
rich.BindRoundImage(m.Avatar, &m.name, true)
|
||||
|
||||
lbl, _ := gtk.LabelNew("")
|
||||
lbl.SetUseMarkup(true)
|
||||
lbl.SetXAlign(0)
|
||||
lbl.SetEllipsize(pango.ELLIPSIZE_END)
|
||||
lbl.Show()
|
||||
m.Name = rich.NewLabel(&m.name)
|
||||
m.Name.SetUseMarkup(true)
|
||||
m.Name.SetXAlign(0)
|
||||
m.Name.SetEllipsize(pango.ELLIPSIZE_END)
|
||||
m.Name.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.PackStart(icon, false, false, 0)
|
||||
box.PackStart(lbl, true, true, 0)
|
||||
box.Show()
|
||||
memberBoxCSS(box)
|
||||
m.Name.SetRenderer(func(rich text.Rich) markup.RenderOutput {
|
||||
out := markup.RenderCmplx(rich)
|
||||
|
||||
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()
|
||||
memberRowCSS(r)
|
||||
r.Add(evb)
|
||||
if !m.second.IsEmpty() {
|
||||
out.Markup += fmt.Sprintf(
|
||||
"\n"+`<span alpha="85%%"><sup>%s</sup></span>`,
|
||||
markup.Render(m.second),
|
||||
)
|
||||
}
|
||||
|
||||
m := &Member{
|
||||
ListBoxRow: r,
|
||||
Main: box,
|
||||
Avatar: icon,
|
||||
Name: lbl,
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
m.Main, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
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)
|
||||
|
||||
return m
|
||||
return &m
|
||||
}
|
||||
|
||||
var noMentionLinks = markup.RenderConfig{
|
||||
|
@ -368,38 +398,22 @@ var noMentionLinks = markup.RenderConfig{
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
func (m *Member) Popup(evq EventQueuer) {
|
||||
if len(m.output.Mentions) == 0 {
|
||||
out := m.Name.Output()
|
||||
|
||||
if len(out.Mentions) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
p := labeluri.NewPopoverMentioner(m, m.output.Input, m.output.Mentions[0])
|
||||
p := labeluri.NewPopoverMentioner(m, out.Input, out.Mentions[0])
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,50 +1,36 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
// Author implements cchat.Author. It effectively contains a copy of
|
||||
// cchat.Author.
|
||||
type Author struct {
|
||||
id cchat.ID
|
||||
name text.Rich
|
||||
avatarURL string
|
||||
ID cchat.ID
|
||||
Name rich.NameContainer
|
||||
}
|
||||
|
||||
var _ cchat.Author = (*Author)(nil)
|
||||
|
||||
// NewAuthor creates a new Author that is a copy of the given author.
|
||||
func NewAuthor(author cchat.Author) Author {
|
||||
a := Author{}
|
||||
a.Update(author)
|
||||
func NewAuthor(author cchat.User) Author {
|
||||
a := Author{ID: author.ID()}
|
||||
a.Name.QueueNamer(context.Background(), author)
|
||||
return a
|
||||
}
|
||||
|
||||
// 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{
|
||||
id,
|
||||
name,
|
||||
avatar,
|
||||
ID: id,
|
||||
Name: rich.NameContainer{LabelState: *rich.NewLabelState(name)},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Author) Update(author cchat.Author) {
|
||||
a.id = author.ID()
|
||||
a.name = author.Name()
|
||||
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
|
||||
// Update sets a new name.
|
||||
func (author *Author) Update(user cchat.Namer) {
|
||||
author.Name.QueueNamer(context.Background(), user)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"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/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"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 {
|
||||
ID() cchat.ID
|
||||
Time() time.Time
|
||||
Author() cchat.Author
|
||||
Nonce() string
|
||||
// Unwrap unwraps the message container and, if revert is true, revert the
|
||||
// state to a clean version. Containers must implement this method by
|
||||
// itself.
|
||||
Unwrap(revert bool) *State
|
||||
|
||||
UpdateAuthor(cchat.Author)
|
||||
UpdateContent(c text.Rich, edited bool)
|
||||
UpdateTimestamp(time.Time)
|
||||
// UpdateContent updates the underlying content widget.
|
||||
UpdateContent(content text.Rich, edited bool)
|
||||
|
||||
// SetReferenceHighlighter sets the reference highlighter into the message.
|
||||
SetReferenceHighlighter(refer labeluri.ReferenceHighlighter)
|
||||
}
|
||||
|
||||
// FillContainer sets the container's contents to the one from MessageCreate.
|
||||
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
|
||||
// State provides a single generic message container for subpackages
|
||||
// to use.
|
||||
type GenericContainer struct {
|
||||
*gtk.Box
|
||||
row *gtk.ListBoxRow // contains Box
|
||||
type State struct {
|
||||
gtk.Box
|
||||
Row *gtk.ListBoxRow // contains Box
|
||||
class string
|
||||
|
||||
id string
|
||||
time time.Time
|
||||
author Author
|
||||
nonce string
|
||||
ID cchat.ID
|
||||
Time time.Time
|
||||
Nonce string
|
||||
Author *Author
|
||||
|
||||
Content *gtk.Box
|
||||
ContentBody *labeluri.Label
|
||||
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
|
||||
// does not update the widgets, so FillContainer should be called afterwards.
|
||||
func NewContainer(msg cchat.MessageCreate) *GenericContainer {
|
||||
c := NewEmptyContainer()
|
||||
c.id = msg.ID()
|
||||
c.time = msg.Time()
|
||||
c.nonce = msg.Nonce()
|
||||
c.author.Update(msg.Author())
|
||||
c := NewEmptyState()
|
||||
c.Author.ID = author.ID()
|
||||
c.Author.Name.QueueNamer(context.Background(), author)
|
||||
c.ID = msg.ID()
|
||||
c.Time = msg.Time()
|
||||
c.Nonce = msg.Nonce()
|
||||
|
||||
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.SetVExpand(true)
|
||||
ctbody.SetHAlign(gtk.ALIGN_START)
|
||||
|
@ -100,9 +96,10 @@ func NewEmptyContainer() *GenericContainer {
|
|||
row.Show()
|
||||
primitives.AddClass(row, "message-row")
|
||||
|
||||
gc := &GenericContainer{
|
||||
Box: box,
|
||||
row: row,
|
||||
gc := &State{
|
||||
Box: *box,
|
||||
Row: row,
|
||||
Author: &Author{},
|
||||
|
||||
Content: ctbox,
|
||||
ContentBody: ctbody,
|
||||
|
@ -110,81 +107,49 @@ func NewEmptyContainer() *GenericContainer {
|
|||
|
||||
// Time is important, as it is used to sort messages, so we have to be
|
||||
// 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.
|
||||
gc.ContentBody.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) {
|
||||
menu.MenuSeparator(m)
|
||||
menu.MenuItems(m, gc.menuItems)
|
||||
menu.MenuItems(m, gc.MenuItems)
|
||||
})
|
||||
|
||||
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.
|
||||
func (m *GenericContainer) SetClass(class string) {
|
||||
func (m *State) SetClass(class string) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func (m *GenericContainer) ID() string {
|
||||
return m.id
|
||||
}
|
||||
|
||||
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)
|
||||
// UpdateContent replaces the internal content and the widget.
|
||||
func (m *State) UpdateContent(content text.Rich, edited bool) {
|
||||
m.ContentBody.SetLabel(content)
|
||||
|
||||
if edited {
|
||||
markup := m.ContentBody.Output().Markup
|
||||
markup += " " + rich.Small("(edited)")
|
||||
m.ContentBody.SetMarkup(markup)
|
||||
m.ContentBody.SetRenderer(func(content text.Rich) markup.RenderOutput {
|
||||
output := markup.RenderCmplx(content)
|
||||
output.Markup += rich.Small(text.Plain("(edited)")).Markup
|
||||
return output
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AttachMenu connects signal handlers to handle a list of menu items from
|
||||
// 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 {
|
||||
func (m *State) Focusable() gtk.IWidget {
|
||||
return m.Content
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"fmt"
|
||||
"html"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"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/primitives"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
|
@ -16,33 +16,51 @@ var EmptyContentPlaceholder = fmt.Sprintf(
|
|||
`<span alpha="25%%">%s</span>`, html.EscapeString("<empty>"),
|
||||
)
|
||||
|
||||
type PresendContainer interface {
|
||||
SetDone(id string)
|
||||
// Presender describes actions doable on a presend message container.
|
||||
type Presender interface {
|
||||
SendingMessage() PresendMessage
|
||||
SetDone(id cchat.ID)
|
||||
SetLoading()
|
||||
SetSentError(err error)
|
||||
}
|
||||
|
||||
// PresendGenericContainer is the generic container with extra methods
|
||||
// implemented for stateful mutability of the generic message container.
|
||||
type GenericPresendContainer struct {
|
||||
*GenericContainer
|
||||
// PresendMessage is an interface for any message about to be sent.
|
||||
type PresendMessage interface {
|
||||
cchat.MessageHeader
|
||||
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()
|
||||
presend input.PresendMessage
|
||||
presend PresendMessage
|
||||
uploads *attachment.MessageUploader
|
||||
}
|
||||
|
||||
var _ PresendContainer = (*GenericPresendContainer)(nil)
|
||||
var (
|
||||
_ Presender = (*PresendState)(nil)
|
||||
)
|
||||
|
||||
func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
|
||||
c := NewEmptyContainer()
|
||||
c.nonce = msg.Nonce()
|
||||
c.UpdateAuthor(msg.Author())
|
||||
c.UpdateTimestamp(msg.Time())
|
||||
type SendMessageData struct {
|
||||
}
|
||||
|
||||
p := &GenericPresendContainer{
|
||||
GenericContainer: c,
|
||||
// NewPresendState creates a new presend state.
|
||||
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,
|
||||
uploads: attachment.NewMessageUploader(msg.Files()),
|
||||
}
|
||||
|
@ -51,14 +69,18 @@ func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
|
|||
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)
|
||||
}
|
||||
|
||||
func (m *GenericPresendContainer) SetDone(id string) {
|
||||
// SetDone sets the status of the state.
|
||||
func (m *PresendState) SetDone(id cchat.ID) {
|
||||
// Apply the received ID.
|
||||
m.id = id
|
||||
m.nonce = ""
|
||||
m.ID = id
|
||||
m.Nonce = ""
|
||||
|
||||
// Reset the state to be normal. Especially setting presend to nil should
|
||||
// free it from memory.
|
||||
|
@ -76,7 +98,8 @@ func (m *GenericPresendContainer) SetDone(id string) {
|
|||
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.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.Content.SetTooltipText(err.Error())
|
||||
|
||||
|
@ -132,6 +156,6 @@ func (m *GenericPresendContainer) SetSentError(err error) {
|
|||
}
|
||||
|
||||
// clearBox clears everything inside the content container.
|
||||
func (m *GenericPresendContainer) clearBox() {
|
||||
func (m *PresendState) clearBox() {
|
||||
primitives.RemoveChildren(m.Content)
|
||||
}
|
||||
|
|
|
@ -84,11 +84,11 @@ func (mc *MessageControl) Enable(msg container.MessageRow, names MessageItemName
|
|||
mc.SetSensitive(true)
|
||||
mc.SetRevealChild(true && !mc.hide)
|
||||
|
||||
items := msg.MenuItems()
|
||||
unwrap := msg.Unwrap(false)
|
||||
|
||||
mc.Reply.bind(menu.FindItemFunc(items, names.Reply))
|
||||
mc.Edit.bind(menu.FindItemFunc(items, names.Edit))
|
||||
mc.Delete.bind(menu.FindItemFunc(items, names.Delete))
|
||||
mc.Reply.bind(menu.FindItemFunc(unwrap.MenuItems, names.Reply))
|
||||
mc.Edit.bind(menu.FindItemFunc(unwrap.MenuItems, names.Edit))
|
||||
mc.Delete.bind(menu.FindItemFunc(unwrap.MenuItems, names.Delete))
|
||||
}
|
||||
|
||||
// SetHidden sets whether or not the control should be hidden.
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
package typing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type typer struct {
|
||||
cchat.User
|
||||
s *rich.NameContainer
|
||||
t time.Time
|
||||
}
|
||||
|
||||
type State struct {
|
||||
// states
|
||||
typers []cchat.Typer
|
||||
typers []typer
|
||||
timeout time.Duration
|
||||
canceler func()
|
||||
invalidated bool
|
||||
|
@ -67,35 +75,54 @@ func (s *State) loop() {
|
|||
|
||||
// Call the event handler if things are invalidated.
|
||||
if s.invalidated {
|
||||
s.changed(s, len(s.typers) == 0)
|
||||
s.update()
|
||||
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.
|
||||
func (s *State) invalidate() {
|
||||
// Sort the list of typers again.
|
||||
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
|
||||
}
|
||||
|
||||
// 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() {
|
||||
defer cancel()
|
||||
defer s.invalidate()
|
||||
|
||||
// If the typer already exists, then pop them to the start of the list.
|
||||
for i, t := range s.typers {
|
||||
if t.ID() == typer.ID() {
|
||||
if t.ID() == user.ID() {
|
||||
s.typers[i] = t
|
||||
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()
|
||||
|
||||
for i, t := range s.typers {
|
||||
if t.ID() == typerID {
|
||||
// Remove the quick way. Sort will take care of ordering.
|
||||
l := len(s.typers) - 1
|
||||
s.typers[i] = s.typers[l]
|
||||
s.typers[l] = nil
|
||||
s.typers = s.typers[:l]
|
||||
|
||||
return
|
||||
if t.ID() != typerID {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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.
|
||||
if len(typers) == 0 || timeout == 0 {
|
||||
return nil, false
|
||||
|
@ -130,14 +162,15 @@ func filterTypers(typers []cchat.Typer, timeout time.Duration) ([]cchat.Typer, b
|
|||
var cut int
|
||||
|
||||
for _, t := range typers {
|
||||
if now.Sub(t.Time()) < timeout {
|
||||
if now.Sub(t.t) < timeout {
|
||||
typers[cut] = t
|
||||
cut++
|
||||
}
|
||||
}
|
||||
|
||||
for i := cut; i < len(typers); i++ {
|
||||
typers[i] = nil
|
||||
typers[i].s.Stop()
|
||||
typers[i] = typer{}
|
||||
}
|
||||
|
||||
var changed = cut != len(typers)
|
||||
|
|
|
@ -137,8 +137,8 @@ func (c *Container) Unborrow() {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Container) RemoveAuthor(author cchat.Author) {
|
||||
c.state.removeTyper(author.ID())
|
||||
func (c *Container) RemoveAuthor(userID cchat.ID) {
|
||||
c.state.removeTyper(userID)
|
||||
}
|
||||
|
||||
func (c *Container) TrySubscribe(svmsg cchat.Messenger) bool {
|
||||
|
@ -155,7 +155,7 @@ var noMentionLinks = markup.RenderConfig{
|
|||
NoMentionLinks: true,
|
||||
}
|
||||
|
||||
func render(typers []cchat.Typer) string {
|
||||
func render(typers []typer) string {
|
||||
// fast path
|
||||
if len(typers) == 0 {
|
||||
return ""
|
||||
|
@ -164,7 +164,7 @@ func render(typers []cchat.Typer) string {
|
|||
var builder strings.Builder
|
||||
|
||||
for i, typer := range typers {
|
||||
output := markup.RenderCmplxWithConfig(typer.Name(), noMentionLinks)
|
||||
output := markup.RenderCmplxWithConfig(typer.s.Label(), noMentionLinks)
|
||||
|
||||
builder.WriteString("<b>")
|
||||
builder.WriteString(output.Markup)
|
||||
|
|
|
@ -15,12 +15,16 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/cozy"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/memberlist"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/sadface"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/typing"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"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/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/handy"
|
||||
"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.
|
||||
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.
|
||||
v.FaceView.SetLoading()
|
||||
v.ctrl.OnMessageBusy()
|
||||
|
@ -289,19 +293,22 @@ func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc travers
|
|||
v.reset()
|
||||
|
||||
// Get the messenger once.
|
||||
var messenger = server.AsMessenger()
|
||||
var messenger = srv.Server.AsMessenger()
|
||||
// Exit if this server is not a messenger.
|
||||
if messenger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
// because new messages created by JoinServer will use this state for things
|
||||
// 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() {
|
||||
// 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)
|
||||
}
|
||||
|
||||
firstID := firstMsg.Unwrap(false).ID
|
||||
|
||||
gts.Async(func() (func(), error) {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
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.
|
||||
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.
|
||||
if author != nil {
|
||||
v.Typing.RemoveAuthor(author)
|
||||
if authorID != "" {
|
||||
v.Typing.RemoveAuthor(authorID)
|
||||
}
|
||||
}
|
||||
|
||||
// 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, "")
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return msg.Author()
|
||||
return msg.Unwrap(false).Author
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return msg.Author().ID() == authorID
|
||||
return msg.Unwrap(false).Author.ID == authorID
|
||||
})
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return msg.Author()
|
||||
state := msg.Unwrap(false)
|
||||
return &state.Author.Name
|
||||
}
|
||||
|
||||
// LatestMessageFrom returns the last message ID with that author.
|
||||
func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
||||
return v.Container.LatestMessageFrom(userID)
|
||||
func (v *View) LatestMessageFrom(userID cchat.ID) container.MessageRow {
|
||||
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.
|
||||
func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendMessageRow) {
|
||||
func (v *View) retryMessage(presend container.PresendMessageRow) {
|
||||
var sender = v.InputView.Sender
|
||||
if sender == nil {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
// states and callbacks.
|
||||
func (v *View) BindMenu(msg container.MessageRow) {
|
||||
state := msg.Unwrap(false)
|
||||
|
||||
// Add 1 for the edit menu item.
|
||||
var mitems = []menu.Item{
|
||||
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.
|
||||
if v.InputView.Editable(msg.ID()) {
|
||||
if v.InputView.Editable(state.ID) {
|
||||
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.
|
||||
if v.hasActions() {
|
||||
var actions = v.actioner.Actions(msg.ID())
|
||||
var actions = v.actioner.Actions(state.ID)
|
||||
var items = make([]menu.Item, len(actions))
|
||||
|
||||
for i, action := range actions {
|
||||
items[i] = v.makeActionItem(action, msg.ID())
|
||||
items[i] = v.makeActionItem(action, state.ID)
|
||||
}
|
||||
|
||||
mitems = append(mitems, items...)
|
||||
}
|
||||
|
||||
msg.AttachMenu(mitems)
|
||||
state.MenuItems = mitems
|
||||
}
|
||||
|
||||
// makeActionItem creates a new menu callback that's called on menu item
|
||||
|
|
|
@ -189,7 +189,7 @@ func (c *Completer) update() []gtk.IWidget {
|
|||
lbox.Show()
|
||||
|
||||
// Label for the primary text.
|
||||
l := rich.NewLabel(entry.Text)
|
||||
l := rich.NewStaticLabel(entry.Text)
|
||||
l.Show()
|
||||
lbox.PackStart(l, false, false, 0)
|
||||
|
||||
|
@ -198,7 +198,7 @@ func (c *Completer) update() []gtk.IWidget {
|
|||
if !entry.Secondary.IsEmpty() {
|
||||
size = ImageLarge
|
||||
|
||||
s := rich.NewLabel(text.Rich{})
|
||||
s := rich.NewStaticLabel(text.Plain(""))
|
||||
s.SetMarkup(fmt.Sprintf(
|
||||
`<span alpha="50%%" size="small">%s</span>`,
|
||||
markup.Render(entry.Secondary),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package primitives
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
|
@ -179,27 +178,13 @@ func NewImageIconPx(icon string, sizepx int) *gtk.Image {
|
|||
}
|
||||
|
||||
type ImageIconSetter interface {
|
||||
SetProperty(name string, value interface{}) error
|
||||
GetSizeRequest() (w, h int)
|
||||
SetSizeRequest(w, h int)
|
||||
SetFromIconName(string, gtk.IconSize)
|
||||
SetPixelSize(int)
|
||||
}
|
||||
|
||||
func SetImageIcon(img ImageIconSetter, icon string, sizepx int) {
|
||||
// Prioritize SetSize()
|
||||
if setter, ok := img.(interface{ SetSize(int) }); ok {
|
||||
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)
|
||||
img.SetFromIconName(icon, gtk.ICON_SIZE_BUTTON)
|
||||
img.SetPixelSize(sizepx)
|
||||
}
|
||||
|
||||
func PrependMenuItems(menu interface{ Prepend(gtk.IMenuItem) }, items []gtk.IMenuItem) {
|
||||
|
@ -240,12 +225,6 @@ type Connector interface {
|
|||
|
||||
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()) {
|
||||
connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) {
|
||||
if gts.EventIsRightClick(ev) {
|
||||
|
|
|
@ -28,6 +28,7 @@ type Avatar struct {
|
|||
pixbuf *gdk.Pixbuf
|
||||
url string
|
||||
size int
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// Make a better API that allows scaling.
|
||||
|
@ -49,6 +50,7 @@ func NewAvatar(size int) *Avatar {
|
|||
return &avatar
|
||||
}
|
||||
|
||||
// GetSizeRequest returns the virtual size.
|
||||
func (a *Avatar) GetSizeRequest() (int, int) {
|
||||
return a.size, a.size
|
||||
}
|
||||
|
@ -66,7 +68,6 @@ func (a *Avatar) SetSizeRequest(w, h int) {
|
|||
}
|
||||
|
||||
func (a *Avatar) loadFunc(size int) *gdk.Pixbuf {
|
||||
// No URL, draw nothing.
|
||||
if a.url == "" {
|
||||
return nil
|
||||
}
|
||||
|
@ -75,44 +76,54 @@ func (a *Avatar) loadFunc(size int) *gdk.Pixbuf {
|
|||
return a.pixbuf
|
||||
}
|
||||
|
||||
// Refetch and rescale.
|
||||
a.size = size
|
||||
// Technically, this will recurse. However, we're changing the size, so
|
||||
// eventually it should stop.
|
||||
httputil.AsyncImage(context.Background(), a, a.url)
|
||||
a.refetch()
|
||||
|
||||
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.
|
||||
func (a *Avatar) SetRadius(float64) {}
|
||||
|
||||
// SetImageURL sets the avatar's source URL and reloads it asynchronously.
|
||||
func (a *Avatar) SetImageURL(url string) {
|
||||
a.url = url
|
||||
a.Avatar.SetImageLoadFunc(a.loadFunc)
|
||||
a.refetch()
|
||||
}
|
||||
|
||||
// SetFromPixbuf sets the pixbuf.
|
||||
func (a *Avatar) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||
a.cancelCtx()
|
||||
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) {
|
||||
a.cancelCtx()
|
||||
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 {
|
||||
return a.pixbuf
|
||||
}
|
||||
|
@ -127,6 +138,7 @@ func (a *Avatar) GetImage() *gtk.Image {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetStorageType always returns IMAGE_PIXBUF.
|
||||
func (a *Avatar) GetStorageType() gtk.ImageType {
|
||||
return gtk.IMAGE_PIXBUF
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
// TODO: move roundimage.Button to rich.ImageButton.
|
||||
|
||||
// Button implements a rounded button with a rounded image. This widget only
|
||||
// supports a full circle for rounding.
|
||||
type Button struct {
|
||||
|
@ -20,7 +22,7 @@ var roundButtonCSS = primitives.PrepareClassCSS("round-button", `
|
|||
`)
|
||||
|
||||
func NewButton() (*Button, error) {
|
||||
image, _ := NewImage(0)
|
||||
image := NewImage(0)
|
||||
image.Show()
|
||||
|
||||
b := NewEmptyButton()
|
||||
|
@ -38,7 +40,7 @@ func NewEmptyButton() *Button {
|
|||
}
|
||||
|
||||
// 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.
|
||||
func NewCustomButton(img Imager) (*Button, error) {
|
||||
b := NewEmptyButton()
|
||||
|
|
|
@ -2,10 +2,12 @@ package roundimage
|
|||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/handy"
|
||||
"github.com/diamondburned/imgutil"
|
||||
"github.com/gotk3/gotk3/cairo"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
|
@ -39,83 +41,201 @@ type Imager interface {
|
|||
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 {
|
||||
*gtk.Image
|
||||
gtk.Image
|
||||
Radius float64
|
||||
|
||||
style *gtk.StyleContext
|
||||
|
||||
procs []imgutil.Processor
|
||||
ifNone func(context.Context)
|
||||
|
||||
icon struct {
|
||||
name string
|
||||
size int
|
||||
}
|
||||
|
||||
cancel context.CancelFunc
|
||||
imgURL string
|
||||
show bool
|
||||
}
|
||||
|
||||
var _ Imager = (*Image)(nil)
|
||||
|
||||
// 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.
|
||||
func NewImage(radius float64) (*Image, error) {
|
||||
func NewImage(radius float64) *Image {
|
||||
i, err := gtk.ImageNew()
|
||||
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.
|
||||
i.Connect("draw", image.drawer)
|
||||
|
||||
// Backup plan if Cairo's Surface is weird.
|
||||
|
||||
// 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
|
||||
return image
|
||||
}
|
||||
|
||||
// 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) {
|
||||
i.procs = append(i.procs, procs...)
|
||||
}
|
||||
|
||||
// GetImage returns the underlying image widget.
|
||||
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) {
|
||||
// No dynamic sizing support; yolo.
|
||||
httputil.AsyncImage(context.Background(), i, url, i.procs...)
|
||||
i.SetImageURLInto(url, i)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
i.Radius = r
|
||||
i.QueueDraw()
|
||||
}
|
||||
|
||||
func (i *Image) drawer(_ interface{}, cc *cairo.Context) bool {
|
||||
var w = float64(i.GetAllocatedWidth())
|
||||
var h = float64(i.GetAllocatedHeight())
|
||||
func (i *Image) drawer(image *gtk.Image, cc *cairo.Context) bool {
|
||||
// Don't round if we're displaying a stock icon.
|
||||
if i.imgURL == "" && i.icon.name != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
var min = w
|
||||
// Use the smallest side for radius calculation.
|
||||
if h < w {
|
||||
a := image.GetAllocation()
|
||||
w := float64(a.GetWidth())
|
||||
h := float64(a.GetHeight())
|
||||
|
||||
min := w
|
||||
// Use the largest side for radius calculation.
|
||||
if h > w {
|
||||
min = h
|
||||
}
|
||||
|
||||
// Copy the variables in case we need to change them.
|
||||
var r = i.Radius
|
||||
|
||||
switch {
|
||||
// If radius is less than 0, then don't round.
|
||||
case r < 0:
|
||||
case i.Radius < 0:
|
||||
return false
|
||||
|
||||
// If radius is 0, then we have to calculate our own radius.:This only
|
||||
// works if the image is a square.
|
||||
case r == 0:
|
||||
case i.Radius == 0:
|
||||
// Calculate the radius by dividing a side by 2.
|
||||
r = (min / 2)
|
||||
r := (min / 2)
|
||||
|
||||
// Draw an arc from 0deg to 360deg.
|
||||
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
|
||||
// the edges.
|
||||
case r > 0:
|
||||
case i.Radius > 0:
|
||||
// StackOverflow is godly.
|
||||
// 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.
|
||||
if max := min / 2; r > max {
|
||||
r = max
|
||||
|
@ -157,3 +280,25 @@ func (i *Image) drawer(_ interface{}, cc *cairo.Context) bool {
|
|||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -1,49 +1,44 @@
|
|||
package roundimage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/gotk3/gotk3/cairo"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
)
|
||||
|
||||
// StaticImage is an image that only plays a GIF if it's hovered on top of.
|
||||
type StaticImage struct {
|
||||
// StillImage is an image that only plays a GIF if it's hovered on top of.
|
||||
type StillImage struct {
|
||||
*Image
|
||||
animating bool
|
||||
animation *gdk.PixbufAnimation
|
||||
}
|
||||
|
||||
var (
|
||||
_ Imager = (*StaticImage)(nil)
|
||||
_ Connector = (*StaticImage)(nil)
|
||||
_ httputil.ImageContainer = (*StaticImage)(nil)
|
||||
_ Imager = (*StillImage)(nil)
|
||||
_ Connector = (*StillImage)(nil)
|
||||
_ httputil.ImageContainer = (*StillImage)(nil)
|
||||
)
|
||||
|
||||
func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage, error) {
|
||||
i, err := NewImage(radius)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// NewStillImage creates a new static that binds to the parent's handler so
|
||||
// that the image only animates when parent is hovered over.
|
||||
func NewStillImage(parent primitives.Connector, radius float64) *StillImage {
|
||||
i := NewImage(radius)
|
||||
|
||||
var s = &StaticImage{i, false, nil}
|
||||
if parent != nil {
|
||||
s.ConnectHandlers(parent)
|
||||
}
|
||||
s := StillImage{i, false, nil}
|
||||
s.ConnectHandlers(parent)
|
||||
|
||||
return s, nil
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
|
||||
connector.Connect("enter-notify-event", func(interface{}) {
|
||||
func (s *StillImage) ConnectHandlers(connector primitives.Connector) {
|
||||
connector.Connect("enter-notify-event", func() {
|
||||
if s.animation != nil && !s.animating {
|
||||
s.animating = true
|
||||
s.Image.SetFromAnimation(s.animation)
|
||||
}
|
||||
})
|
||||
connector.Connect("leave-notify-event", func(interface{}) {
|
||||
connector.Connect("leave-notify-event", func() {
|
||||
if s.animation != nil && s.animating {
|
||||
s.animating = false
|
||||
s.Image.SetFromPixbuf(s.animation.GetStaticImage())
|
||||
|
@ -51,26 +46,26 @@ func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
|
|||
})
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetImageURL(url string) {
|
||||
// No dynamic sizing support; yolo.
|
||||
httputil.AsyncImage(context.Background(), s, url, s.Image.procs...)
|
||||
// SetImageURL sets the image's URL.
|
||||
func (s *StillImage) SetImageURL(url string) {
|
||||
s.Image.SetImageURLInto(url, s)
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||
func (s *StillImage) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||
s.animation = nil
|
||||
s.Image.SetFromPixbuf(pb)
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetFromSurface(sf *cairo.Surface) {
|
||||
func (s *StillImage) SetFromSurface(sf *cairo.Surface) {
|
||||
s.animation = nil
|
||||
s.Image.SetFromSurface(sf)
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetFromAnimation(anim *gdk.PixbufAnimation) {
|
||||
func (s *StillImage) SetFromAnimation(anim *gdk.PixbufAnimation) {
|
||||
s.animation = anim
|
||||
s.Image.SetFromPixbuf(anim.GetStaticImage())
|
||||
}
|
||||
|
||||
func (s *StaticImage) GetAnimation() *gdk.PixbufAnimation {
|
||||
func (s *StillImage) GetAnimation() *gdk.PixbufAnimation {
|
||||
return s.animation
|
||||
}
|
||||
|
|
|
@ -1,219 +1,35 @@
|
|||
package rich
|
||||
|
||||
import (
|
||||
"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
|
||||
import "log"
|
||||
|
||||
// ImageURLSetter describes an image that can be set a URL.
|
||||
type ImageURLSetter interface {
|
||||
SetImageURL(url string)
|
||||
|
||||
GetStorageType() gtk.ImageType
|
||||
GetPixbuf() *gdk.Pixbuf
|
||||
GetAnimation() *gdk.PixbufAnimation
|
||||
}
|
||||
|
||||
var (
|
||||
_ RoundIconContainer = (*roundimage.Image)(nil)
|
||||
_ RoundIconContainer = (*roundimage.StaticImage)(nil)
|
||||
)
|
||||
|
||||
// Icon represents a rounded image container.
|
||||
type Icon struct {
|
||||
*gtk.Revealer // TODO move out
|
||||
|
||||
Image RoundIconContainer
|
||||
|
||||
// state
|
||||
url string
|
||||
type tooltipSetter interface {
|
||||
SetTooltipText(text 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)
|
||||
|
||||
func NewIcon(sizepx int) *Icon {
|
||||
img, _ := roundimage.NewImage(0)
|
||||
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")
|
||||
if tooltip {
|
||||
tooltipper, ok := img.(tooltipSetter)
|
||||
if !ok {
|
||||
log.Panicf("img of type %T is not tooltipSetter", img)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,134 +1,94 @@
|
|||
package rich
|
||||
|
||||
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/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Labeler interface {
|
||||
// thread-safe
|
||||
cchat.LabelContainer // thread-safe
|
||||
// LabelRenderer is the input/output function to render a rich text segment to
|
||||
// Pango markup.
|
||||
type LabelRenderer = func(text.Rich) markup.RenderOutput
|
||||
|
||||
// not thread-safe
|
||||
SetLabelUnsafe(text.Rich)
|
||||
GetLabel() text.Rich
|
||||
GetText() string
|
||||
}
|
||||
|
||||
// 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
|
||||
// RenderSkipImages is a label renderer that skips images.
|
||||
func RenderSkipImages(rich text.Rich) markup.RenderOutput {
|
||||
return markup.RenderCmplxWithConfig(rich, markup.RenderConfig{
|
||||
SkipImages: true,
|
||||
NoMentionLinks: true,
|
||||
})
|
||||
}
|
||||
|
||||
// SetLabel is thread-safe.
|
||||
func (l *Label) SetLabel(content text.Rich) {
|
||||
gts.ExecAsync(func() { l.SetLabelUnsafe(content) })
|
||||
// Label provides an abstraction around a regular GTK label that can be
|
||||
// self-updated. Widgets that extend off of this (such as ToggleButton) does not
|
||||
// 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
|
||||
// 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
|
||||
var _ gtk.IWidget = (*Label)(nil)
|
||||
|
||||
if l.validsuper() {
|
||||
l.super.SetLabelUnsafe(content)
|
||||
} else {
|
||||
l.SetMarkup(markup.Render(content))
|
||||
// NewStaticLabel creates a static, non-updating label.
|
||||
func NewStaticLabel(rich text.Rich) *Label {
|
||||
label, _ := gtk.LabelNew("")
|
||||
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.
|
||||
func (l *Label) GetLabel() text.Rich {
|
||||
return l.Current
|
||||
// NewLabel creates a self-updating label.
|
||||
func NewLabel(state LabelStateStorer) *Label {
|
||||
return NewLabelWithRenderer(state, nil)
|
||||
}
|
||||
|
||||
// GetText is NOT thread-safe.
|
||||
func (l *Label) GetText() string {
|
||||
return l.Current.Content
|
||||
// NewLabelWithRenderer creates a self-updating label using the given renderer.
|
||||
func NewLabelWithRenderer(state LabelStateStorer, r LabelRenderer) *Label {
|
||||
l := NewStaticLabel(text.Plain(""))
|
||||
l.render = r
|
||||
state.OnUpdate(func() { l.SetLabel(state.Label()) })
|
||||
return l
|
||||
}
|
||||
|
||||
type ToggleButton struct {
|
||||
gtk.ToggleButton
|
||||
Label
|
||||
// Output returns the rendered output.
|
||||
func (l *Label) Output() markup.RenderOutput {
|
||||
return l.output
|
||||
}
|
||||
|
||||
var (
|
||||
_ gtk.IWidget = (*ToggleButton)(nil)
|
||||
_ cchat.LabelContainer = (*ToggleButton)(nil)
|
||||
)
|
||||
// SetLabel sets the label in the current thread, meaning it's not thread-safe.
|
||||
func (l *Label) SetLabel(content text.Rich) {
|
||||
// Save a call if the content is empty.
|
||||
if content.IsEmpty() {
|
||||
l.label = content
|
||||
l.output = markup.RenderOutput{}
|
||||
|
||||
func NewToggleButton(content text.Rich) *ToggleButton {
|
||||
l := NewLabel(content)
|
||||
l.Show()
|
||||
return
|
||||
}
|
||||
|
||||
b, _ := gtk.ToggleButtonNew()
|
||||
primitives.BinLeftAlignLabel(b)
|
||||
l.label = content
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -31,69 +31,35 @@ const (
|
|||
MaxHeight = 500
|
||||
)
|
||||
|
||||
type WidgetConnector interface {
|
||||
gtk.IWidget
|
||||
primitives.Connector
|
||||
}
|
||||
|
||||
var _ WidgetConnector = (*gtk.Label)(nil)
|
||||
|
||||
// Labeler implements a rich label that stores an output state.
|
||||
type Labeler interface {
|
||||
WidgetConnector
|
||||
rich.Labeler
|
||||
Output() markup.RenderOutput
|
||||
}
|
||||
// // 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.
|
||||
type Label struct {
|
||||
*rich.Label
|
||||
*BoundBox
|
||||
output markup.RenderOutput
|
||||
}
|
||||
|
||||
var (
|
||||
_ Labeler = (*Label)(nil)
|
||||
_ rich.SuperLabeler = (*Label)(nil)
|
||||
)
|
||||
// var (
|
||||
// _ Labeler = (*Label)(nil)
|
||||
// _ rich.SuperLabeler = (*Label)(nil)
|
||||
// )
|
||||
|
||||
func NewLabel(txt text.Rich) *Label {
|
||||
l := &Label{}
|
||||
l.Label = rich.NewInheritLabel(l)
|
||||
l.Label.SetLabelUnsafe(txt) // test
|
||||
l.Label = rich.NewStaticLabel(txt)
|
||||
|
||||
// Bind and return.
|
||||
l.BoundBox = BindRichLabel(l)
|
||||
l.BoundBox = BindRichLabel(l.Label)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Label) Reset() {
|
||||
l.output = markup.RenderOutput{}
|
||||
}
|
||||
|
||||
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
|
||||
l.Label.SetLabel(text.Plain(""))
|
||||
}
|
||||
|
||||
type ReferenceHighlighter interface {
|
||||
|
@ -103,11 +69,11 @@ type ReferenceHighlighter interface {
|
|||
// BoundBox is a box wrapping elements that can be interacted with from the
|
||||
// parsed labels.
|
||||
type BoundBox struct {
|
||||
label Labeler
|
||||
label *rich.Label
|
||||
refer ReferenceHighlighter
|
||||
}
|
||||
|
||||
func BindRichLabel(label Labeler) *BoundBox {
|
||||
func BindRichLabel(label *rich.Label) *BoundBox {
|
||||
bound := BoundBox{label: label}
|
||||
bind(label, bound.activate)
|
||||
return &bound
|
||||
|
@ -206,7 +172,7 @@ func NewPopoverMentioner(rel gtk.IWidget, input string, segment text.Segment) *g
|
|||
l.Show()
|
||||
|
||||
// Enable images???
|
||||
BindActivator(l)
|
||||
BindImagePreview(l)
|
||||
|
||||
// Make a scrolling text.
|
||||
scr := scrollinput.NewVScroll(PopoverWidth)
|
||||
|
@ -259,13 +225,23 @@ func popoverImg(url string, round bool) gtk.IWidget {
|
|||
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 connects activate-link. If activator returns true, then nothing is done.
|
||||
// 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
|
||||
// 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
|
||||
|
@ -273,11 +249,11 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle)
|
|||
// message, but we're also keeping alive the widget.
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
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.
|
||||
r := gdk.Rectangle{}
|
||||
r.SetX(int(x))
|
||||
|
@ -297,8 +273,8 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle)
|
|||
if !round {
|
||||
img, _ = gtk.ImageNew()
|
||||
} else {
|
||||
r, _ := roundimage.NewImage(0)
|
||||
img = r.Image
|
||||
r := roundimage.NewImage(0)
|
||||
img = &r.Image
|
||||
}
|
||||
|
||||
img.SetSizeRequest(w, h)
|
||||
|
|
|
@ -123,6 +123,10 @@ type RenderConfig struct {
|
|||
// mentions.
|
||||
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
|
||||
// the boolean is true. Else, all mention links will not work and regular
|
||||
// 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.
|
||||
if start == end {
|
||||
if !cfg.SkipImages && start == end {
|
||||
if imager := segment.AsImager(); imager != nil {
|
||||
appended.Open(start, composeImageMarkup(imager))
|
||||
}
|
||||
|
|
|
@ -1,22 +1,236 @@
|
|||
package rich
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/pkg/errors"
|
||||
)
|
||||
|
||||
func Small(text string) string {
|
||||
return `<span size="small" color="#808080">` + text + "</span>"
|
||||
// Small is a renderer that makes the plain text small.
|
||||
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 {
|
||||
return `<span color="red">` + html.EscapeString(content.Content) + `</span>`
|
||||
// MakeRed is a renderer that makes the plain text red.
|
||||
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
|
||||
type nullLabel struct {
|
||||
text.Rich
|
||||
// LabelStateStorer is an interface for LabelState.
|
||||
type LabelStateStorer interface {
|
||||
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{}
|
||||
}
|
||||
|
|
|
@ -4,15 +4,12 @@ package config
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"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/primitives/menu"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
@ -31,17 +28,17 @@ func Restore(conf Configurator) {
|
|||
gts.Async(func() (func(), error) {
|
||||
c, err := conf.Configuration()
|
||||
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)
|
||||
|
||||
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 {
|
||||
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
|
||||
|
@ -52,16 +49,16 @@ func Spawn(conf Configurator) error {
|
|||
gts.Async(func() (func(), error) {
|
||||
c, err := conf.Configuration()
|
||||
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)
|
||||
|
||||
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() {
|
||||
spawn(conf.Name().String(), c, func(finalized bool) error {
|
||||
spawn(conf, c, func(finalized bool) error {
|
||||
if err := conf.SetConfiguration(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -81,16 +78,10 @@ func Spawn(conf Configurator) error {
|
|||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
func spawn(c Configurator, conf map[string]string, apply func(final bool) error) {
|
||||
container := newContainer(conf, func() error { return apply(false) })
|
||||
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.Show()
|
||||
|
||||
var title = "Configure " + name
|
||||
|
||||
h, _ := gtk.HeaderBarNew()
|
||||
h.SetTitle(title)
|
||||
h.SetTitle("Configure " + c.ID())
|
||||
h.SetShowCloseButton(true)
|
||||
h.Show()
|
||||
|
||||
var state rich.NameContainer
|
||||
state.OnUpdate(func() {
|
||||
h.SetTitle("Configure " + state.String())
|
||||
})
|
||||
|
||||
d, _ := gts.NewEmptyModalDialog()
|
||||
d.SetDefaultSize(400, 300)
|
||||
d.Add(b)
|
||||
d.SetTitle(title)
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -131,7 +131,10 @@ type sizeBinder interface {
|
|||
var _ sizeBinder = (*List)(nil)
|
||||
|
||||
func (h *Header) AppMenuBindSize(c sizeBinder) {
|
||||
c.Connect("size-allocate", func(c sizeBinder) {
|
||||
update := func(c sizeBinder) {
|
||||
h.AppMenu.SetSizeRequest(c.GetAllocatedWidth(), -1)
|
||||
})
|
||||
}
|
||||
|
||||
c.Connect("size-allocate", update)
|
||||
update(c)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,8 @@ var _ ListController = (*List)(nil)
|
|||
var listCSS = primitives.PrepareClassCSS("service-list", `
|
||||
.service-list {
|
||||
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); */
|
||||
}
|
||||
`)
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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/roundimage"
|
||||
"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/session"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
|
@ -19,7 +19,7 @@ import (
|
|||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
const IconSize = 48
|
||||
const IconSize = 42
|
||||
|
||||
type ListController interface {
|
||||
// ClearMessenger is called when a nil slice of servers is set.
|
||||
|
@ -43,13 +43,14 @@ type Service struct {
|
|||
|
||||
*gtk.Box
|
||||
Button *gtk.ToggleButton
|
||||
Icon *rich.Icon
|
||||
Icon *roundimage.StillImage
|
||||
Menu *actions.Menu
|
||||
|
||||
BodyRev *gtk.Revealer // revealed
|
||||
BodyList *session.List // not really supposed to be here
|
||||
|
||||
service cchat.Service // state
|
||||
Name rich.NameContainer
|
||||
Configurator cchat.Configurator
|
||||
}
|
||||
|
||||
|
@ -67,7 +68,7 @@ var serviceCSS = primitives.PrepareClassCSS("service", `
|
|||
|
||||
var serviceButtonCSS = primitives.PrepareClassCSS("service-button", `
|
||||
.service-button {
|
||||
padding: 2px;
|
||||
padding: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -78,58 +79,51 @@ var serviceButtonCSS = primitives.PrepareClassCSS("service-button", `
|
|||
|
||||
.service-button:checked {
|
||||
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", `
|
||||
.service-icon { padding: 4px }
|
||||
`)
|
||||
var serviceIconCSS = primitives.PrepareClassCSS("service-icon", ``)
|
||||
|
||||
func NewService(svc cchat.Service, svclctrl ListController) *Service {
|
||||
service := &Service{
|
||||
service := Service{
|
||||
service: svc,
|
||||
ListController: svclctrl,
|
||||
}
|
||||
service.Name.QueueNamer(context.Background(), svc)
|
||||
|
||||
service.BodyList = session.NewList(service)
|
||||
service.BodyList = session.NewList(&service)
|
||||
service.BodyList.Show()
|
||||
|
||||
service.BodyRev, _ = gtk.RevealerNew()
|
||||
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.Add(service.BodyList)
|
||||
service.BodyRev.Show()
|
||||
|
||||
// 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.Add(service.Icon)
|
||||
service.Button.SetRelief(gtk.RELIEF_NONE)
|
||||
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) {
|
||||
revealed := !service.GetRevealChild()
|
||||
service.SetRevealChild(revealed)
|
||||
tb.SetActive(revealed)
|
||||
})
|
||||
serviceButtonCSS(service.Button)
|
||||
|
||||
// Bind session.* actions into row.
|
||||
service.Menu = actions.NewMenu("service")
|
||||
|
@ -153,9 +147,9 @@ func NewService(svc cchat.Service, svclctrl ListController) *Service {
|
|||
serviceCSS(service.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.
|
||||
|
@ -204,7 +198,7 @@ func (s *Service) AddSession(ses cchat.Session) *session.Row {
|
|||
}
|
||||
|
||||
func (s *Service) ID() string {
|
||||
return s.service.Name().Content
|
||||
return s.service.ID()
|
||||
}
|
||||
|
||||
func (s *Service) Service() cchat.Service {
|
||||
|
@ -232,7 +226,7 @@ func (s *Service) MoveSession(id, movingID string) {
|
|||
}
|
||||
|
||||
func (s *Service) Breadcrumb() string {
|
||||
return s.service.Name().Content
|
||||
return s.Name.String()
|
||||
}
|
||||
|
||||
func (s *Service) ParentBreadcrumb() traverse.Breadcrumber {
|
||||
|
@ -241,15 +235,13 @@ func (s *Service) ParentBreadcrumb() traverse.Breadcrumber {
|
|||
|
||||
func (s *Service) SaveAllSessions() {
|
||||
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 {
|
||||
if k := keyring.ConvertSession(s.Session); k != nil {
|
||||
keyrings = append(keyrings, *k)
|
||||
}
|
||||
for _, session := range sessions {
|
||||
keyrings.Add(session.Session, s.Name.String())
|
||||
}
|
||||
|
||||
keyring.SaveSessions(s.service, keyrings)
|
||||
keyrings.Save()
|
||||
}
|
||||
|
||||
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(
|
||||
"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
|
||||
}
|
||||
|
||||
// Session is not a pointer, so we can pass it into arguments safely.
|
||||
for _, ses := range keyring.RestoreSessions(s.service) {
|
||||
service := keyring.Restore(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.RestoreSession(rs, ses)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
package button
|
||||
|
||||
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/roundimage"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
|
@ -13,20 +11,32 @@ const UnreadColorDefs = `
|
|||
@define-color mentioned rgb(240, 71, 71);
|
||||
`
|
||||
|
||||
type ToggleButtonImage struct {
|
||||
*rich.ToggleButtonImage
|
||||
const IconSize = 38
|
||||
|
||||
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)
|
||||
readcss primitives.ClassEnum
|
||||
|
||||
err error
|
||||
icon string // whether or not the button has an icon
|
||||
iconSz int
|
||||
icon string // whether or not the button has an icon
|
||||
label bool
|
||||
}
|
||||
|
||||
var _ cchat.IconContainer = (*ToggleButtonImage)(nil)
|
||||
|
||||
var serverButtonCSS = primitives.PrepareClassCSS("server-button", `
|
||||
.server-button {
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
.selected-server {
|
||||
border-left: 2px solid 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 {
|
||||
color: alpha(@theme_fg_color, 0.5);
|
||||
/* color: alpha(@theme_fg_color, 0.5); */
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.unread {
|
||||
color: @theme_fg_color;
|
||||
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 {
|
||||
|
@ -52,34 +62,55 @@ var serverButtonCSS = primitives.PrepareClassCSS("server-button", `
|
|||
|
||||
`+UnreadColorDefs)
|
||||
|
||||
func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
||||
b := rich.NewToggleButtonImage(content)
|
||||
return WrapToggleButtonImage(b)
|
||||
}
|
||||
// NewToggleButton creates a new toggle button.
|
||||
func NewToggleButton(state rich.LabelStateStorer) *ToggleButton {
|
||||
label := rich.NewLabelWithRenderer(state, rich.RenderSkipImages)
|
||||
label.SetMarginStart(5)
|
||||
label.Show()
|
||||
|
||||
func WrapToggleButtonImage(b *rich.ToggleButtonImage) *ToggleButtonImage {
|
||||
b.Show()
|
||||
labelRev, _ := gtk.RevealerNew()
|
||||
labelRev.Add(label)
|
||||
labelRev.SetRevealChild(true)
|
||||
labelRev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
||||
labelRev.SetTransitionDuration(100)
|
||||
labelRev.Show()
|
||||
|
||||
tb := &ToggleButtonImage{
|
||||
ToggleButtonImage: b,
|
||||
clicked: func(bool) {},
|
||||
}
|
||||
|
||||
type activeGetter interface {
|
||||
GetActive() bool
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.SetHAlign(gtk.ALIGN_START)
|
||||
box.PackStart(labelRev, false, false, 0)
|
||||
box.Show()
|
||||
|
||||
button, _ := gtk.ToggleButtonNew()
|
||||
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()) })
|
||||
serverButtonCSS(tb)
|
||||
|
||||
// Ensure that we display an icon when we receive one.
|
||||
state.OnUpdate(func() {
|
||||
if state.Image().HasImage() {
|
||||
tb.ensureImage()
|
||||
}
|
||||
})
|
||||
|
||||
return tb
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetSelected(selected bool) {
|
||||
// Set the clickability the opposite as the boolean.
|
||||
// b.SetSensitive(!selected)
|
||||
b.SetInconsistent(selected)
|
||||
|
||||
// SetSelected sets the button's intermediate state and appearance to look
|
||||
// like it's clicked without triggering the callback.
|
||||
func (b *ToggleButton) SetSelected(selected bool) {
|
||||
if selected {
|
||||
primitives.AddClass(b, "selected-server")
|
||||
} 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
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetClickedIfTrue(clickedIfTrue func()) {
|
||||
func (b *ToggleButton) SetClickedIfTrue(clickedIfTrue func()) {
|
||||
b.clicked = func(clicked bool) {
|
||||
if clicked {
|
||||
clickedIfTrue()
|
||||
|
@ -104,33 +160,74 @@ func (b *ToggleButtonImage) SetClickedIfTrue(clickedIfTrue func()) {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetNormal() {
|
||||
b.SetLabelUnsafe(b.GetLabel())
|
||||
// b.menu.SetItems(b.extraMenu)
|
||||
func (b *ToggleButton) ensureImage() {
|
||||
if b.Image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if b.icon != "" {
|
||||
b.Image.SetPlaceholderIcon(b.icon, b.Image.Size())
|
||||
b.Image = roundimage.NewStillImage(b, 0)
|
||||
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() {
|
||||
b.SetLabelUnsafe(b.GetLabel())
|
||||
// SetLoading sets the button's state to loading.
|
||||
func (b *ToggleButton) SetLoading() {
|
||||
b.Label.SetRenderer(rich.RenderSkipImages)
|
||||
b.SetSensitive(false)
|
||||
|
||||
if b.icon != "" {
|
||||
b.Image.SetPlaceholderIcon("content-loading-symbolic", b.Image.Size())
|
||||
if b.Image != nil && b.icon != "" {
|
||||
b.SetPlaceholderIcon("content-loading-symbolic", b.Image.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetFailed(err error, retry func()) {
|
||||
b.Label.SetMarkup(rich.MakeRed(b.GetLabel()))
|
||||
func (b *ToggleButton) SetFailed(err error, retry func()) {
|
||||
b.Label.SetRenderer(rich.MakeRed)
|
||||
b.SetSensitive(true)
|
||||
|
||||
// If we have an icon set, then we can use the failed icon.
|
||||
if b.icon != "" {
|
||||
b.Image.SetPlaceholderIcon("computer-fail-symbolic", b.Image.Size())
|
||||
if b.Image != nil && b.icon != "" {
|
||||
b.SetPlaceholderIcon("computer-fail-symbolic", b.Image.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetUnreadUnsafe(unread, mentioned bool) {
|
||||
func (b *ToggleButton) SetUnreadUnsafe(unread, mentioned bool) {
|
||||
switch {
|
||||
// Prioritize mentions over unreads.
|
||||
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.ensureImage()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package button
|
||||
|
||||
type CollapsibleLabel struct{}
|
|
@ -13,22 +13,28 @@ import (
|
|||
)
|
||||
|
||||
type Controller interface {
|
||||
ClearMessenger()
|
||||
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 will contain hollow rows. They are rows that do not yet have any
|
||||
// widgets. This changes as soon as Row's Load is called.
|
||||
type Children struct {
|
||||
*gtk.Box
|
||||
gtk.Box
|
||||
Controller
|
||||
|
||||
load *loading.Button // only not nil while loading
|
||||
loading bool
|
||||
|
||||
Rows []*ServerRow
|
||||
Rows []*ServerRow
|
||||
expand bool
|
||||
|
||||
Parent traverse.Breadcrumber
|
||||
rowctrl Controller
|
||||
Parent traverse.Breadcrumber
|
||||
|
||||
// Unreadable state for children rows to use. The parent row that has this
|
||||
// Children will bind a handler to this.
|
||||
|
@ -37,8 +43,6 @@ type Children struct {
|
|||
|
||||
var childrenCSS = primitives.PrepareClassCSS("server-children", `
|
||||
.server-children {
|
||||
margin: 0;
|
||||
margin-top: 3px;
|
||||
border-radius: 0;
|
||||
}
|
||||
`)
|
||||
|
@ -47,8 +51,8 @@ var childrenCSS = primitives.PrepareClassCSS("server-children", `
|
|||
// widgets.
|
||||
func NewHollowChildren(p traverse.Breadcrumber, ctrl Controller) *Children {
|
||||
return &Children{
|
||||
Parent: p,
|
||||
rowctrl: ctrl,
|
||||
Parent: p,
|
||||
Controller: ctrl,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +64,7 @@ func NewChildren(p traverse.Breadcrumber, ctrl Controller) *Children {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -70,10 +74,13 @@ func (c *Children) IsHollow() bool {
|
|||
// Nothing but ServerRow should call this method.
|
||||
func (c *Children) Init() {
|
||||
if c.IsHollow() {
|
||||
c.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
c.Box.SetMarginStart(ChildrenMargin)
|
||||
c.Box.SetHExpand(true)
|
||||
childrenCSS(c.Box)
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.SetMarginStart(ChildrenMargin)
|
||||
c.Box = *box
|
||||
childrenCSS(box)
|
||||
|
||||
// Show all margins by default.
|
||||
c.SetExpand(true)
|
||||
|
||||
// Check if we're still loading. This is effectively restoring the
|
||||
// state that was set before we had widgets.
|
||||
|
@ -90,7 +97,7 @@ func (c *Children) Init() {
|
|||
func (c *Children) Reset() {
|
||||
// If the children container isn't hollow, then we have to remove the known
|
||||
// rows from the container box.
|
||||
if c.Box != nil {
|
||||
if c.Box.Object != nil {
|
||||
// Remove old servers from the list.
|
||||
for _, row := range c.Rows {
|
||||
if row.IsHollow() {
|
||||
|
@ -104,6 +111,26 @@ func (c *Children) Reset() {
|
|||
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
|
||||
// will only update the state.
|
||||
func (c *Children) setLoading() {
|
||||
|
@ -168,7 +195,7 @@ func (c *Children) SetServersUnsafe(servers []cchat.Server) {
|
|||
if server == nil {
|
||||
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.
|
||||
|
@ -205,7 +232,7 @@ func (c *Children) UpdateServerUnsafe(update cchat.ServerUpdate) {
|
|||
prevID, replace := update.PreviousID()
|
||||
|
||||
// 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)
|
||||
|
||||
// If we're appending a new row, then replace is false.
|
||||
|
@ -246,23 +273,13 @@ func (c *Children) LoadAll() {
|
|||
// Restore expansion if possible.
|
||||
savepath.Restore(row, row.Button)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have icons.
|
||||
var hasIcon bool
|
||||
|
||||
// ForceIcons forces all of the children's row to show icons.
|
||||
func (c *Children) ForceIcons() {
|
||||
for _, row := range c.Rows {
|
||||
if row.HasIcon() {
|
||||
hasIcon = true
|
||||
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()
|
||||
}
|
||||
row.UseEmptyIcon()
|
||||
row.SetShowLabel(c.expand)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,19 +5,20 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
// Buffer represents an unbuffered API around the text buffer.
|
||||
type Buffer struct {
|
||||
*gtk.TextBuffer
|
||||
name string
|
||||
name rich.LabelStateStorer
|
||||
cmder cchat.Commander
|
||||
}
|
||||
|
||||
// NewBuffer creates a new buffer with the given SessionCommander, or returns
|
||||
// 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 {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -39,10 +39,14 @@ func SpawnDialog(buf *Buffer) {
|
|||
s.Show()
|
||||
|
||||
h, _ := gtk.HeaderBarNew()
|
||||
h.SetTitle("Commander: " + buf.name)
|
||||
h.SetShowCloseButton(true)
|
||||
h.Show()
|
||||
|
||||
rm := buf.name.OnUpdate(func() {
|
||||
h.SetTitle("Commander: " + buf.name.Label().Content)
|
||||
})
|
||||
h.Connect("destroy", rm)
|
||||
|
||||
d, _ := gts.NewEmptyModalDialog()
|
||||
d.SetDefaultSize(450, 250)
|
||||
d.SetTitlebar(h)
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/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/service/savepath"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/button"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/commander"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const ChildrenMargin = 24
|
||||
const IconSize = 32
|
||||
const ChildrenMargin = 0 // refer to style.css
|
||||
|
||||
func AssertUnhollow(hollower interface{ IsHollow() bool }) {
|
||||
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 {
|
||||
*gtk.Box
|
||||
Avatar *roundimage.Avatar
|
||||
Button *button.ToggleButtonImage
|
||||
Button *button.ToggleButton
|
||||
ActionsMenu *actions.Menu
|
||||
|
||||
Server cchat.Server
|
||||
ctrl Controller
|
||||
name rich.NameContainer
|
||||
ctrl ParentController
|
||||
|
||||
parentcrumb traverse.Breadcrumber
|
||||
|
||||
|
@ -46,10 +52,12 @@ type ServerRow struct {
|
|||
childrev *gtk.Revealer
|
||||
children *Children
|
||||
serverList cchat.Lister
|
||||
serverStop func()
|
||||
|
||||
// State that's updated even when stale. Initializations will use these.
|
||||
unread bool
|
||||
mentioned bool
|
||||
showLabel bool
|
||||
|
||||
// callback to cancel unread indicator
|
||||
cancelUnread func()
|
||||
|
@ -59,25 +67,28 @@ var serverCSS = primitives.PrepareClassCSS("server", `
|
|||
/* Ignore first child because .server-children already covers this */
|
||||
.server:not(:first-child) {
|
||||
margin: 0;
|
||||
margin-top: 3px;
|
||||
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
|
||||
// hollow children containers and rows for the given server.
|
||||
func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl Controller) *ServerRow {
|
||||
var serverRow = &ServerRow{
|
||||
func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl ParentController) *ServerRow {
|
||||
serverRow := ServerRow{
|
||||
parentcrumb: p,
|
||||
ctrl: ctrl,
|
||||
Server: sv,
|
||||
cancelUnread: func() {},
|
||||
}
|
||||
|
||||
var (
|
||||
lister = sv.AsLister()
|
||||
messenger = sv.AsMessenger()
|
||||
)
|
||||
serverRow.name.QueueNamer(context.Background(), sv)
|
||||
|
||||
lister := sv.AsLister()
|
||||
messenger := sv.AsMessenger()
|
||||
|
||||
switch {
|
||||
case lister != nil:
|
||||
|
@ -87,7 +98,7 @@ func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl Controller)
|
|||
case messenger != nil:
|
||||
if unreader := messenger.AsUnreadIndicator(); unreader != nil {
|
||||
gts.Async(func() (func(), error) {
|
||||
c, err := unreader.UnreadIndicate(serverRow)
|
||||
c, err := unreader.UnreadIndicate(&serverRow)
|
||||
if err != nil {
|
||||
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),
|
||||
|
@ -108,19 +119,13 @@ func (r *ServerRow) Init() {
|
|||
}
|
||||
|
||||
// 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())
|
||||
btn.Show()
|
||||
|
||||
r.Button = button.WrapToggleButtonImage(btn)
|
||||
r.Button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
r.Button.SetRelief(gtk.RELIEF_NONE)
|
||||
r.Button = button.NewToggleButton(&r.name)
|
||||
r.Button.SetShowLabel(r.showLabel)
|
||||
r.Button.Show()
|
||||
|
||||
r.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
r.Box.SetHAlign(gtk.ALIGN_FILL)
|
||||
r.Box.PackStart(r.Button, false, false, 0)
|
||||
serverCSS(r.Box)
|
||||
|
||||
|
@ -131,9 +136,6 @@ func (r *ServerRow) Init() {
|
|||
// Ensure errors are displayed.
|
||||
r.childrenSetErr(r.childrenErr)
|
||||
|
||||
// Try to set an icon.
|
||||
r.SetIconer(r.Server)
|
||||
|
||||
// Connect the destroyer, if any.
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
lister = r.Server.AsLister()
|
||||
columnate = lister != nil && lister.Columnate()
|
||||
messenger = r.Server.AsMessenger()
|
||||
)
|
||||
|
||||
switch {
|
||||
case lister != nil:
|
||||
case lister != nil && !columnate:
|
||||
primitives.AddClass(r, "server-list")
|
||||
r.children.Init()
|
||||
r.children.Show()
|
||||
|
@ -171,15 +181,35 @@ func (r *ServerRow) Init() {
|
|||
r.Box.PackStart(r.childrev, false, false, 0)
|
||||
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:
|
||||
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.
|
||||
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
|
||||
// state. This obviously means nothing is being selected.
|
||||
if r.Button == nil {
|
||||
|
@ -196,7 +226,7 @@ func (r *ServerRow) SetUnread(unread, mentioned bool) {
|
|||
|
||||
func (r *ServerRow) SetUnreadUnsafe(unread, mentioned bool) {
|
||||
// We're never unread if we're reading this current server.
|
||||
if r.GetActiveServerMessage() {
|
||||
if r.IsActiveServerMessage() {
|
||||
unread, mentioned = false, false
|
||||
}
|
||||
|
||||
|
@ -243,12 +273,14 @@ func (r *ServerRow) load(finish func(error)) {
|
|||
}
|
||||
|
||||
go func() {
|
||||
var err = list.Servers(children)
|
||||
stop, err := list.Servers(children)
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get servers"))
|
||||
}
|
||||
|
||||
gts.ExecAsync(func() {
|
||||
r.serverStop = stop
|
||||
|
||||
// Announce that we're not loading anymore.
|
||||
r.children.setNotLoading()
|
||||
|
||||
|
@ -273,6 +305,11 @@ func (r *ServerRow) Reset() {
|
|||
r.children.Destroy()
|
||||
}
|
||||
|
||||
if r.serverStop != nil {
|
||||
r.serverStop()
|
||||
r.serverStop = nil
|
||||
}
|
||||
|
||||
// Reset the state.
|
||||
r.ActionsMenu.Reset()
|
||||
r.serverList = nil
|
||||
|
@ -298,14 +335,36 @@ func (r *ServerRow) childrenSetErr(err error) {
|
|||
// UseEmptyIcon forces the row to show a placeholder icon.
|
||||
func (r *ServerRow) UseEmptyIcon() {
|
||||
AssertUnhollow(r)
|
||||
|
||||
r.Button.Image.SetSize(IconSize)
|
||||
r.Button.Image.SetRevealChild(true)
|
||||
r.Button.UseEmptyIcon()
|
||||
}
|
||||
|
||||
// HasIcon returns true if the current row has an icon.
|
||||
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 {
|
||||
|
@ -316,28 +375,18 @@ func (r *ServerRow) Breadcrumb() string {
|
|||
if r.IsHollow() {
|
||||
return ""
|
||||
}
|
||||
return r.Button.GetText()
|
||||
|
||||
return r.name.String()
|
||||
}
|
||||
|
||||
// ID returns the server ID.
|
||||
func (r *ServerRow) ID() cchat.ID {
|
||||
return r.Server.ID()
|
||||
}
|
||||
|
||||
func (r *ServerRow) SetLabelUnsafe(name text.Rich) {
|
||||
AssertUnhollow(r)
|
||||
|
||||
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")
|
||||
}
|
||||
// Name returns the name state.
|
||||
func (r *ServerRow) Name() rich.LabelStateStorer {
|
||||
return &r.name
|
||||
}
|
||||
|
||||
// SetLoading is called by the parent struct.
|
||||
|
@ -357,7 +406,6 @@ func (r *ServerRow) SetFailed(err error, retry func()) {
|
|||
r.SetSensitive(true)
|
||||
r.SetTooltipText(err.Error())
|
||||
r.Button.SetFailed(err, retry)
|
||||
r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel()))
|
||||
r.ActionsMenu.Reset()
|
||||
r.ActionsMenu.AddAction("Retry", retry)
|
||||
}
|
||||
|
@ -372,14 +420,6 @@ func (r *ServerRow) SetDone() {
|
|||
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.
|
||||
func (r *ServerRow) SetSelected(selected bool) {
|
||||
AssertUnhollow(r)
|
||||
|
@ -411,6 +451,12 @@ func (r *ServerRow) SetRevealChild(reveal bool) {
|
|||
// Save the path.
|
||||
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 !reveal {
|
||||
return
|
||||
|
|
|
@ -22,6 +22,8 @@ type Breadcrumber interface {
|
|||
type BreadcrumbNamer interface {
|
||||
// Breadcrumb returns the breadcrumb name.
|
||||
Breadcrumb() string
|
||||
|
||||
// TODO: make BreadcrumbNamer return LabelState.
|
||||
}
|
||||
|
||||
// Traverse traverses the given breadcrumber recursively. If traverser returns
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
"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/traverse"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/serverpane"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
)
|
||||
|
@ -20,59 +21,102 @@ const ListWidth = 200
|
|||
// SessionController extends server.Controller to add needed methods that the
|
||||
// specific top-level servers container needs.
|
||||
type SessionController interface {
|
||||
server.Controller
|
||||
ClearMessenger()
|
||||
MessengerSelected(*server.ServerRow)
|
||||
}
|
||||
|
||||
// Servers wraps around a list of servers inherited from Children. It's the
|
||||
// container that's displayed on the right of the service sidebar.
|
||||
// Servers wraps around a list of servers inherited from Children to display a
|
||||
// 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 {
|
||||
*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
|
||||
spinner *spinner.Boxed // non-nil if loading.
|
||||
|
||||
ctrl SessionController
|
||||
|
||||
// state
|
||||
ServerList cchat.Lister
|
||||
// NextColumn is main's rhs.
|
||||
NextColumn *Servers // nil
|
||||
detachNext func()
|
||||
}
|
||||
|
||||
var toplevelCSS = primitives.PrepareClassCSS("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 {
|
||||
c := server.NewChildren(p, ctrl)
|
||||
c.SetMarginStart(0) // children is top level; there is no main row
|
||||
c.SetVExpand(true)
|
||||
c.Show()
|
||||
toplevelCSS(c)
|
||||
|
||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
|
||||
return &Servers{
|
||||
Box: b,
|
||||
Children: c,
|
||||
ctrl: ctrl,
|
||||
servers := Servers{
|
||||
SessionController: 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() {
|
||||
// Reset isn't necessarily called while loading, so we do a check.
|
||||
if s.spinner != nil {
|
||||
s.spinner.Stop()
|
||||
s.spinner.Destroy()
|
||||
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.
|
||||
s.ServerList = nil
|
||||
// Remove all children.
|
||||
primitives.RemoveChildren(s)
|
||||
s.Lister = nil
|
||||
// Reset the children container.
|
||||
s.Children.Reset()
|
||||
s.Stack.SetVisibleChild(s.Main)
|
||||
}
|
||||
|
||||
// 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
|
||||
// server.Children, this method will load immediately.
|
||||
func (s *Servers) SetList(slist cchat.Lister) {
|
||||
primitives.RemoveChildren(s)
|
||||
s.ServerList = slist
|
||||
if s.stopLs != nil {
|
||||
s.stopLs()
|
||||
s.stopLs = nil
|
||||
}
|
||||
|
||||
s.Lister = slist
|
||||
s.load()
|
||||
}
|
||||
|
||||
|
@ -98,11 +146,12 @@ func (s *Servers) load() {
|
|||
s.setLoading()
|
||||
|
||||
go func() {
|
||||
err := s.ServerList.Servers(s)
|
||||
stop, err := s.Lister.Servers(s)
|
||||
gts.ExecAsync(func() {
|
||||
if err != nil {
|
||||
s.setFailed(err)
|
||||
} else {
|
||||
s.stopLs = stop
|
||||
s.setDone()
|
||||
}
|
||||
})
|
||||
|
@ -114,8 +163,8 @@ func (s *Servers) SetServers(servers []cchat.Server) {
|
|||
gts.ExecAsync(func() {
|
||||
s.Children.SetServersUnsafe(servers)
|
||||
|
||||
if len(servers) == 0 {
|
||||
s.ctrl.ClearMessenger()
|
||||
if servers == nil {
|
||||
s.ClearMessenger()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -131,37 +180,38 @@ func (s *Servers) UpdateServer(update cchat.ServerUpdate) {
|
|||
|
||||
// setDone changes the view to show the servers.
|
||||
func (s *Servers) setDone() {
|
||||
primitives.RemoveChildren(s)
|
||||
s.SetVisibleChild(s.Main)
|
||||
|
||||
// stop the spinner.
|
||||
s.spinner.Stop()
|
||||
s.spinner.Destroy()
|
||||
s.spinner = nil
|
||||
|
||||
s.Add(s.Children)
|
||||
}
|
||||
|
||||
// setLoading shows a loading spinner. Use this after the session row is
|
||||
// connected.
|
||||
func (s *Servers) setLoading() {
|
||||
primitives.RemoveChildren(s)
|
||||
|
||||
s.spinner = spinner.New()
|
||||
s.spinner.SetSizeRequest(FaceSize, FaceSize)
|
||||
s.spinner.Show()
|
||||
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
|
||||
// failed to load.
|
||||
func (s *Servers) setFailed(err error) {
|
||||
primitives.RemoveChildren(s)
|
||||
|
||||
// stop the spinner. Let this SEGFAULT if nil, as that's undefined behavior.
|
||||
s.spinner.Stop()
|
||||
// stop the spinner. Let this SEGFAULT if nil.
|
||||
s.spinner.Destroy()
|
||||
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.
|
||||
ltop, _ := gtk.LabelNew("")
|
||||
ltop.Show()
|
||||
|
@ -182,7 +232,49 @@ func (s *Servers) setFailed(err error) {
|
|||
lerr.Show()
|
||||
|
||||
// Add these items into the box.
|
||||
s.PackStart(ltop, false, false, 0)
|
||||
s.PackStart(btn, false, false, 10) // pad
|
||||
s.PackStart(lerr, false, false, 0)
|
||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
b.PackStart(ltop, false, false, 0)
|
||||
b.PackStart(btn, false, false, 10) // pad
|
||||
b.PackStart(lerr, false, false, 0)
|
||||
b.Show()
|
||||
|
||||
s.Stack.AddNamed(b, "error")
|
||||
s.Stack.SetVisibleChildName("error")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"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/drag"
|
||||
"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/parser/markup"
|
||||
"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/commander"
|
||||
|
@ -55,9 +55,9 @@ type Controller interface {
|
|||
// Row represents a session row entry in the session List.
|
||||
type Row struct {
|
||||
*gtk.ListBoxRow
|
||||
avatar *roundimage.Avatar
|
||||
name rich.NameContainer
|
||||
iconBox *gtk.EventBox
|
||||
icon *rich.Icon // nillable
|
||||
icon *roundimage.StillImage // nillable
|
||||
|
||||
ctrl Controller
|
||||
parentcrumb traverse.Breadcrumber
|
||||
|
@ -80,6 +80,10 @@ type Row struct {
|
|||
var rowCSS = primitives.PrepareClassCSS("session-row",
|
||||
button.UnreadColorDefs+`
|
||||
|
||||
.session-row {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.session-row:last-child {
|
||||
border-radius: 0 0 14px 14px;
|
||||
}
|
||||
|
@ -117,32 +121,21 @@ var rowCSS = primitives.PrepareClassCSS("session-row",
|
|||
}
|
||||
`)
|
||||
|
||||
var rowIconCSS = primitives.PrepareClassCSS("session-icon", `
|
||||
.session-icon {
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
`)
|
||||
var rowIconCSS = primitives.PrepareClassCSS("session-icon", ``)
|
||||
|
||||
const IconSize = 48
|
||||
const 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
|
||||
}
|
||||
const (
|
||||
IconSize = 42
|
||||
IconName = "face-plain-symbolic"
|
||||
)
|
||||
|
||||
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)
|
||||
return 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.SetLoading()
|
||||
return row
|
||||
|
@ -154,17 +147,32 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Controller) *Row
|
|||
parentcrumb: parent,
|
||||
}
|
||||
|
||||
row.avatar = roundimage.NewAvatar(IconSize)
|
||||
row.avatar.SetText(name.Content)
|
||||
row.avatar.Show()
|
||||
if !name.IsEmpty() {
|
||||
row.name.LabelState = *rich.NewLabelState(name)
|
||||
}
|
||||
|
||||
row.iconBox, _ = gtk.EventBoxNew()
|
||||
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.Add(row.iconBox)
|
||||
row.ListBoxRow.Show()
|
||||
rowCSS(row.ListBoxRow)
|
||||
|
||||
row.name.OnUpdate(func() {
|
||||
// TODO: proper popovers instead of tooltips.
|
||||
row.ListBoxRow.SetTooltipText(row.name.String())
|
||||
})
|
||||
|
||||
// TODO: commander button
|
||||
|
||||
row.Servers = NewServers(row, row)
|
||||
|
@ -216,6 +224,11 @@ func NewAddButton() *gtk.ListBoxRow {
|
|||
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.
|
||||
// It resets all states back to nil, but the session ID stays.
|
||||
func (r *Row) Reset() {
|
||||
|
@ -223,14 +236,10 @@ func (r *Row) Reset() {
|
|||
r.ActionsMenu.Reset() // wipe menu items
|
||||
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.
|
||||
r.icon.SetPlaceholderIcon("folder-remote-symbolic", IconSize)
|
||||
|
||||
r.name.Stop()
|
||||
r.Session = nil
|
||||
r.cmder = nil
|
||||
}
|
||||
|
@ -243,7 +252,8 @@ func (r *Row) Breadcrumb() string {
|
|||
if r.Session == nil {
|
||||
return ""
|
||||
}
|
||||
return r.Session.Name().Content
|
||||
|
||||
return r.name.String()
|
||||
}
|
||||
|
||||
func (r *Row) ClearMessenger() {
|
||||
|
@ -273,25 +283,11 @@ func (r *Row) Activate() {
|
|||
func (r *Row) SetLoading() {
|
||||
// Reset the state.
|
||||
r.Session = nil
|
||||
|
||||
// Reset the icon.
|
||||
primitives.RemoveChildren(r.iconBox)
|
||||
r.icon = nil
|
||||
|
||||
// Remove everything from the row, including the icon.
|
||||
primitives.RemoveChildren(r)
|
||||
r.name.Stop()
|
||||
|
||||
// Remove the failed class.
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -303,18 +299,8 @@ func (r *Row) SetFailed(err error) {
|
|||
r.Session = nil
|
||||
// Re-enable the row.
|
||||
r.SetSensitive(true)
|
||||
// Remove everything off the row.
|
||||
primitives.RemoveChildren(r)
|
||||
// Mark the row as 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.
|
||||
r.icon.SetPlaceholderIcon("view-refresh-symbolic", IconSize)
|
||||
}
|
||||
|
@ -338,25 +324,11 @@ func (r *Row) SetSession(ses cchat.Session) {
|
|||
// Set the states.
|
||||
r.Session = ses
|
||||
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)
|
||||
|
||||
// If the session has an icon, then use it.
|
||||
if iconer := ses.AsIconer(); iconer != nil {
|
||||
r.icon.AsyncSetIconer(iconer, "failed to set session icon")
|
||||
}
|
||||
r.name.QueueNamer(context.Background(), ses)
|
||||
|
||||
// Update to indicate that we're done.
|
||||
primitives.RemoveChildren(r)
|
||||
r.SetSensitive(true)
|
||||
r.Add(r.iconBox)
|
||||
|
||||
// Bind extra menu items before loading. These items won't be clickable
|
||||
// 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
|
||||
// be 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
|
||||
// commander.
|
||||
r.ActionsMenu.AddAction("Command Prompt", r.ShowCommander)
|
||||
|
|
|
@ -3,8 +3,6 @@ package service
|
|||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"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/server"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
|
@ -36,10 +34,6 @@ type View struct {
|
|||
|
||||
Services *List
|
||||
ServerView *gtk.ScrolledWindow
|
||||
|
||||
ServerStack *singlestack.Stack
|
||||
|
||||
// Servers *session.Servers // nil by default; use .Servers
|
||||
}
|
||||
|
||||
func NewView(ctrller Controller) *View {
|
||||
|
@ -52,18 +46,10 @@ func NewView(ctrller Controller) *View {
|
|||
view.Header.AppMenuBindSize(view.Services)
|
||||
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.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
view.ServerView.Add(view.ServerStack)
|
||||
view.ServerView.SetHExpand(true)
|
||||
view.ServerView.SetVExpand(true)
|
||||
view.ServerView.Show()
|
||||
|
||||
view.BottomPane, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
|
@ -95,14 +81,8 @@ func (v *View) SessionSelected(svc *Service, srow *session.Row) {
|
|||
}
|
||||
}
|
||||
|
||||
// !!!: SHITTY HACK!!!
|
||||
// We can do this, as we're keeping all the server lists in memory by Go's
|
||||
// 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())
|
||||
}
|
||||
primitives.RemoveChildren(v.ServerView)
|
||||
v.ServerView.Add(srow.Servers)
|
||||
|
||||
v.Header.SetSessionMenu(srow)
|
||||
v.Header.SetBreadcrumber(srow)
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -17,28 +17,20 @@ import (
|
|||
"github.com/gotk3/gotk3/glib"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed style.css
|
||||
var styleCSS string
|
||||
|
||||
func init() {
|
||||
// Load the local CSS.
|
||||
gts.LoadCSS("main", `
|
||||
/* 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 }
|
||||
`)
|
||||
gts.LoadCSS("main", styleCSS)
|
||||
}
|
||||
|
||||
// constraints for the left panel
|
||||
const leftCurrentWidth = 300
|
||||
const leftCurrentWidth = 350
|
||||
|
||||
func clamp(n, min, max int) int {
|
||||
switch {
|
||||
|
@ -186,7 +178,7 @@ func (app *App) MessengerSelected(ses *session.Row, srv *server.ServerRow) {
|
|||
app.lastSelector = srv.SetSelected
|
||||
app.lastSelector(true)
|
||||
|
||||
app.MessageView.JoinServer(ses.Session, srv.Server, srv)
|
||||
app.MessageView.JoinServer(ses, srv, srv)
|
||||
}
|
||||
|
||||
// MessageView methods.
|
||||
|
@ -207,8 +199,8 @@ func (app *App) OnMessageDone() {
|
|||
}
|
||||
|
||||
func (app *App) AuthenticateSession(list *service.List, ssvc *service.Service) {
|
||||
var svc = ssvc.Service()
|
||||
auth.NewDialog(svc.Name(), svc.Authenticate(), func(ses cchat.Session) {
|
||||
svc := ssvc.Service()
|
||||
auth.NewDialog(ssvc.Name.Label(), svc.Authenticate(), func(ses cchat.Session) {
|
||||
ssvc.AddSession(ses)
|
||||
})
|
||||
}
|
||||
|
@ -219,14 +211,12 @@ func (app *App) Close() {
|
|||
// done, the application would exit immediately. There's no need to update
|
||||
// the GUI.
|
||||
for _, s := range app.Services.Services.Services {
|
||||
var service = s.Service().Name()
|
||||
|
||||
for _, session := range s.BodyList.Sessions() {
|
||||
if session.Session == nil {
|
||||
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 {
|
||||
log.Error(errors.Wrap(err, "Failed to disconnect "+session.ID()))
|
||||
|
|
4
main.go
4
main.go
|
@ -10,11 +10,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
||||
"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-mock"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -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))
|
||||
}()
|
||||
}
|
24
shell.nix
24
shell.nix
|
@ -1,31 +1,11 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
let nostrip = pkg: pkgs.enableDebugging (pkg.overrideAttrs(old: {
|
||||
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 {
|
||||
pkgs.stdenv.mkDerivation rec {
|
||||
name = "cchat-gtk";
|
||||
version = "0.0.2";
|
||||
|
||||
buildInputs = [
|
||||
libhandy
|
||||
pkgs.libhandy
|
||||
pkgs.gnome3.gspell
|
||||
pkgs.gnome3.glib
|
||||
pkgs.gnome3.gtk
|
||||
|
|
Loading…
Reference in New Issue