Merge pull request #1 from diamondburned/wip

Minor refactors, UI changes, and added full typing indicator
This commit is contained in:
diamondburned 2020-07-03 21:43:17 -07:00 committed by GitHub
commit 3cb3ad9852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 657 additions and 246 deletions

10
go.mod
View File

@ -7,13 +7,11 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200630
require (
github.com/Xuanwo/go-locale v0.2.0
github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.0.39
github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846
github.com/diamondburned/cchat-mock v0.0.0-20200630025821-605d61d89288
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64
github.com/diamondburned/cchat v0.0.40
github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3
github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516
github.com/goodsign/monday v1.0.0
github.com/google/btree v1.0.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20200424224625-be1b05b0b279

63
go.sum
View File

@ -21,11 +21,14 @@ github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00
github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
github.com/Xuanwo/go-locale v0.2.0 h1:1N8SGG2VNpLl6VVa8ueZm3Nm+dxvk8ffY9aviKHl4IE=
github.com/Xuanwo/go-locale v0.2.0/go.mod h1:6qbT9M726OJgyiGZro2YwPmx63wQzlH+VvtjJWQoftw=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI=
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/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/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -39,48 +42,20 @@ github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb h1:Ja/niwykeFoSkYxdRRzM8QUAuCswfLmaiBTd2UIU+54=
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb/go.mod h1:q1MbMBfZrv7xqV8n7LgMwhHs3oBbNwWJes8exs2AmDs=
github.com/diamondburned/arikawa v0.9.5 h1:P1ffsp+NHT22wWKYFVC8CdlGRLzPuUV9FcCBKOCJpCI=
github.com/diamondburned/arikawa v0.9.5/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
github.com/diamondburned/cchat v0.0.28 h1:+1VnltW0rl8/NZTUP+x89jVhi3YTTR+e6iLprZ7HcwM=
github.com/diamondburned/cchat v0.0.28/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.31 h1:yUgrh5xbGX0R55glyxYtVewIDL2eXLJ+okIEfVaVoFk=
github.com/diamondburned/cchat v0.0.31/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.32 h1:nLiD4sL9+DLBnvNb9XLidd5peRzTgM9lWcqRsUmm474=
github.com/diamondburned/cchat v0.0.32/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.34 h1:BGiVxMRA9dmW3rLilIldBvjVan7eTTpaWCCfX9IKBYU=
github.com/diamondburned/cchat v0.0.34/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.35 h1:WiMGl8BQJgbP9E4xRxgLGlqUsHpTcJgDKDt8/6a7lBk=
github.com/diamondburned/cchat v0.0.35/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.37 h1:yGz9yls5Lb/vLkU/DU53GjC80WOqoRe229DXsu5mtaY=
github.com/diamondburned/cchat v0.0.37/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.39 h1:Hxd7swmAIECm0MBd5wb1IFvreChwDFwnAshqgAstWGA=
github.com/diamondburned/cchat v0.0.39/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3 h1:8RCcaY3gtA+8NG2mwkcC/PIFK+eS8XnGyeVaUbCXbF0=
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3/go.mod h1:4q0jHEl1gJEzkS92oacwcSf9+3fFcNPukOpURDJpV/A=
github.com/diamondburned/cchat-discord v0.0.0-20200629023132-0a66a65fcf27 h1:MAWs83jAZKxkU04RAoaewc1xUrLKlB6F5X1ViFRkWdw=
github.com/diamondburned/cchat-discord v0.0.0-20200629023132-0a66a65fcf27/go.mod h1:af673uNyL2NSjYqJ54DotPV/iNY+OvBcQfDqHefbap4=
github.com/diamondburned/cchat-discord v0.0.0-20200630031444-53111f3186b3 h1:NDh2osOfPBtiNzz8ro8P7vN4f91uuVMMBJ8+Q2s/EQo=
github.com/diamondburned/cchat-discord v0.0.0-20200630031444-53111f3186b3/go.mod h1:hj6qS/5TOiIxwWyFMts51ILzY9M3GKbXT31hjVbr9gM=
github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846 h1:LLgI89H9A4ZVf8U7bHmOSkG12b8B6a9dwo32Bq7+0AI=
github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846/go.mod h1:lLWRY7l6cC3bI2ge+Vc7pEhgbIL8XYCeXt+h1PWA/1k=
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d h1:LkzARyvdGRvAsaKEPTV3XcqMHENH6J+KRAI+3sq41Qs=
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d/go.mod h1:SVTt5je4G+re8aSVJAFk/x8vvbRzXdpKgSKmVGoM1tg=
github.com/diamondburned/cchat-mock v0.0.0-20200620231423-b286a0301190 h1:mHbA/xhL584IToD38m3QkeU1cRWIPzBZCDFi1Wv0Bz4=
github.com/diamondburned/cchat-mock v0.0.0-20200620231423-b286a0301190/go.mod h1:u4aWUu4be+JkuMtTIGJNS79T1b+8ruijVn9qL/LM4Rk=
github.com/diamondburned/cchat-mock v0.0.0-20200628063912-3155c1b6d6a9 h1:PnVUCrLTsQlafutS13ST7292WkBpiMJzA7125q02LkA=
github.com/diamondburned/cchat-mock v0.0.0-20200628063912-3155c1b6d6a9/go.mod h1:kKtLZzvkJdeLpiNVmwe15Bl202gfbIHC/LwWUbsSnls=
github.com/diamondburned/cchat-mock v0.0.0-20200630025821-605d61d89288 h1:ApNV7DUyj+LG5lUxtTv4yvQZcprsDgZji0iiD+LYJmM=
github.com/diamondburned/cchat-mock v0.0.0-20200630025821-605d61d89288/go.mod h1:Tu+8b1iz9NGeQb2jmndXn+dQ9zBUa8a8ktK9hL5aaxw=
github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6 h1:ZzLrfQqszhzWI7zqwltzQIWtppfcL7m2aIEpB4kuqx0=
github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/cchat v0.0.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0BPH6I=
github.com/diamondburned/cchat v0.0.40/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03 h1:F5TL7GPRU/D4ldVkS0haY3SiHPtf1Kby/4nbYpm//MQ=
github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03/go.mod h1:p0X6QUH0mxK8yEW0+a4QA77ClAmoxz8CvgbnobMtWQA=
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3 h1:xr07/2cwINyrMqh92pQQJVDfQqG0u6gHAK+ZcGfpSew=
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3/go.mod h1:SRu3OOeggELFr2Wd3/+SpYV1eNcvSk2LBhM70NOZSG8=
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI=
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 h1:/ykUYHuYyj+NN/aaqe6lfaCZQc3EMZs93wAGVJTh5j0=
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
github.com/diamondburned/ningen v0.1.0 h1:cTnRNrN0g2Wr/kgjLLpa3pqlbEd6JPNa1yGDer8uV4U=
github.com/diamondburned/ningen v0.1.0/go.mod h1:1vi8mlBlM2xjJy+IugU51q+IMgyNXggS4Xv3SPFd2Q4=
github.com/diamondburned/ningen v0.1.1-0.20200621003851-e5908c53bf21 h1:luSZOjnwoHCbuw5mG3muuDSOUv5YCbFbbjw17dKZ1Ik=
github.com/diamondburned/ningen v0.1.1-0.20200621003851-e5908c53bf21/go.mod h1:1vi8mlBlM2xjJy+IugU51q+IMgyNXggS4Xv3SPFd2Q4=
github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516 h1:6j4oZahbNdVhSEInRfeYbgDpx1FXDfJy6CcUVyWOuVY=
github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I=
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
@ -125,8 +100,6 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
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=
@ -146,15 +119,21 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
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=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
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/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=
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/markbates/pkger v0.17.0 h1:RFfyBPufP2V6cddUyyEVSHBpaAnM1WzaMNyqomeT+iY=
github.com/markbates/pkger v0.17.0/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
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=
@ -166,6 +145,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
@ -262,12 +242,15 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=

View File

@ -1,7 +1,6 @@
package gts
import (
"context"
"os"
"time"
@ -159,93 +158,21 @@ func ExecSync(fn func()) <-chan struct{} {
return ch
}
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
// main loop.
func AfterFunc(d time.Duration, f func()) (stop func()) {
h, err := glib.TimeoutAdd(
uint(d.Milliseconds()),
func() bool { f(); return true },
)
if err != nil {
panic(err)
}
return func() { glib.SourceRemove(h) }
}
func EventIsRightClick(ev *gdk.Event) bool {
keyev := gdk.EventButtonNewFromEvent(ev)
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
}
// Reuser is an interface for structs that inherit Reusable.
type Reuser interface {
Context() context.Context
Acquire() int64
Validate(int64) bool
}
type AsyncUser = func(context.Context) (interface{}, error)
// AsyncUse is a handler for structs that implement the Reuser primitive. The
// passed in function will be called asynchronously, but swap will be called in
// the Gtk main thread.
func AsyncUse(r Reuser, swap func(interface{}), fn AsyncUser) {
// Acquire an ID.
id := r.Acquire()
ctx := r.Context()
Async(func() (func(), error) {
// Run the callback asynchronously.
v, err := fn(ctx)
if err != nil {
return nil, err
}
return func() {
// Validate the ID. Cancel if it's invalid.
if !r.Validate(id) {
log.Println("Async function value dropped for reusable primitive.")
return
}
// Update the resource.
swap(v)
}, nil
})
}
// Reusable is the synchronization primitive to provide a method for
// asynchronous cancellation and reusability.
//
// It works by copying the ID (time) for each asynchronous operation. The
// operation then completes, and the ID is then compared again before being
// used. It provides a cancellation abstraction around the Gtk main thread.
//
// This struct is not thread-safe, as it relies on the Gtk main thread
// synchronization.
type Reusable struct {
time int64 // creation time, used as ID
ctx context.Context
cancel func()
}
func NewReusable() *Reusable {
r := &Reusable{}
r.Invalidate()
return r
}
// Invalidate generates a new ID for the primitive, which would render
// asynchronously updating elements invalid.
func (r *Reusable) Invalidate() {
// Cancel the old context.
if r.cancel != nil {
r.cancel()
}
// Reset.
r.time = time.Now().UnixNano()
r.ctx, r.cancel = context.WithCancel(context.Background())
}
// Context returns the reusable's cancellable context. It never returns nil.
func (r *Reusable) Context() context.Context {
return r.ctx
}
// Reusable checks the acquired ID against the current one.
func (r *Reusable) Validate(acquired int64) (valid bool) {
return r.time == acquired
}
// Acquire lends the ID to be given to Reusable() after finishing.
func (r *Reusable) Acquire() int64 {
return r.time
}

View File

@ -57,7 +57,7 @@ func AsyncImageSized(img ImageContainerSizer, url string, w, h int, procs ...img
}
// Add a processor to resize.
procs = imgutil.Prepend(imgutil.Resize(w, h), procs)
procs = append(procs, imgutil.Resize(w, h))
ctx, cancel := context.WithCancel(context.Background())
connectDestroyer(img, cancel)

View File

@ -3,7 +3,6 @@ package container
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
"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/service/menu"
@ -47,7 +46,6 @@ type Container interface {
DeleteMessageUnsafe(cchat.MessageDelete)
Reset()
ScrollToBottom()
// AddPresendMessage adds and displays an unsent message.
AddPresendMessage(msg input.PresendMessage) PresendGridMessage
@ -59,6 +57,11 @@ type Container interface {
type Controller interface {
// BindMenu expects the controller to add actioner into the message.
BindMenu(GridMessage)
// Bottomed returns whether or not the message scroller is at the bottom.
Bottomed() bool
// AuthorEvent is called on message create/update. This is used to update
// the typer state.
AuthorEvent(a cchat.MessageAuthor)
}
// Constructor is an interface for making custom message implementations which
@ -73,8 +76,8 @@ const ColumnSpacing = 10
// GridContainer is an implementation of Container, which allows flexible
// message grids.
type GridContainer struct {
*autoscroll.ScrolledWindow
*GridStore
Controller
}
// gridMessage w/ required internals
@ -86,16 +89,9 @@ type gridMessage struct {
var _ Container = (*GridContainer)(nil)
func NewGridContainer(constr Constructor, ctrl Controller) *GridContainer {
store := NewGridStore(constr, ctrl)
sw := autoscroll.NewScrolledWindow()
sw.Add(store.Grid)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
sw.Show()
return &GridContainer{
ScrolledWindow: sw,
GridStore: store,
GridStore: NewGridStore(constr, ctrl),
Controller: ctrl,
}
}
@ -106,7 +102,7 @@ func (c *GridContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
c.GridStore.CreateMessageUnsafe(msg)
// Determine if the user is scrolled to the bottom for cleaning up.
if !c.ScrolledWindow.Bottomed {
if !c.Bottomed() {
return
}
@ -136,9 +132,3 @@ func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
gts.ExecAsync(func() { c.DeleteMessageUnsafe(msg) })
}
// Reset is not thread-safe.
func (c *GridContainer) Reset() {
c.GridStore.Reset()
c.ScrolledWindow.Bottomed = true
}

View File

@ -128,7 +128,7 @@ func (c *Container) CreateMessage(msg cchat.MessageCreate) {
// Did the handler wipe old messages? It will only do so if the user is
// scrolled to the bottom.
if !c.ScrolledWindow.Bottomed {
if !c.Bottomed() {
// If we're not at the bottom, then we exit.
return
}

View File

@ -11,7 +11,7 @@ import (
)
type GridStore struct {
Grid *gtk.Grid
*gtk.Grid
Construct Constructor
Controller Controller
@ -26,7 +26,6 @@ func NewGridStore(constr Constructor, ctrl Controller) *GridStore {
grid.SetRowSpacing(5)
grid.SetMarginStart(5)
grid.SetMarginEnd(5)
grid.SetMarginBottom(5)
grid.Show()
primitives.AddClass(grid, "message-grid")
@ -230,6 +229,9 @@ func (c *GridStore) AddPresendMessage(msg input.PresendMessage) PresendGridMessa
}
func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
// Attempt to update before insertion (aka upsert).
if msgc := c.Message(msg); msgc != nil {
msgc.UpdateAuthor(msg.Author())
@ -253,6 +255,9 @@ func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
}
func (c *GridStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
if msgc := c.Message(msg); msgc != nil {
if author := msg.Author(); author != nil {
msgc.UpdateAuthor(author)

View File

@ -4,6 +4,8 @@ import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
@ -20,16 +22,37 @@ type InputView struct {
Completer *completion.View
}
var textCSS = primitives.PrepareCSS(`
.message-input, .message-input * {
background-color: transparent;
}
.message-input * {
background-color: @theme_base_color;
border: 1px solid alpha(@theme_fg_color, 0.2);
border-radius: 4px;
transition: linear 50ms border-color;
}
.message-input:focus * {
border-color: @theme_selected_bg_color;
}
`)
func NewView(ctrl Controller) *InputView {
text, _ := gtk.TextViewNew()
text.SetSensitive(false)
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
text.SetProperty("top-margin", inputmargin)
text.SetProperty("left-margin", inputmargin)
text.SetProperty("right-margin", inputmargin)
text.SetProperty("bottom-margin", inputmargin)
text.SetVAlign(gtk.ALIGN_START)
text.SetProperty("top-margin", 4)
text.SetProperty("bottom-margin", 4)
text.SetProperty("left-margin", 8)
text.SetProperty("right-margin", 8)
text.Show()
primitives.AddClass(text, "message-input")
primitives.AttachCSS(text, textCSS)
// Bind the text event handler to text first.
c := completion.New(text)
@ -37,12 +60,7 @@ func NewView(ctrl Controller) *InputView {
f := NewField(text, ctrl)
f.Show()
// // Connect to the field's revealer. On resize, we want the autocompleter to
// // have the right padding too.
// f.username.Connect("size-allocate", func(w gtk.IWidget) {
// // Set the autocompleter's left margin to be the same.
// c.SetMarginStart(w.ToWidget().GetAllocatedWidth())
// })
primitives.AddClass(f, "input-field")
return &InputView{f, c}
}
@ -57,7 +75,7 @@ func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageS
type Field struct {
*gtk.Box
username *usernameContainer
Username *username.Container
TextScroll *gtk.ScrolledWindow
text *gtk.TextView
@ -73,10 +91,14 @@ type Field struct {
editingID string // never empty
}
const inputmargin = 4
var scrollinputCSS = primitives.PrepareCSS(`
.scrolled-input {
margin: 5px;
}
`)
func NewField(text *gtk.TextView, ctrl Controller) *Field {
username := newUsernameContainer()
username := username.NewContainer()
username.Show()
buf, _ := text.GetBuffer()
@ -84,14 +106,18 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
sw := scrollinput.NewV(text, 150)
sw.Show()
primitives.AddClass(sw, "scrolled-input")
primitives.AttachCSS(sw, scrollinputCSS)
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(username, false, false, 0)
box.PackStart(sw, true, true, 0)
box.Show()
field := &Field{
Box: box,
username: username,
Box: box,
Username: username,
// typing: typing,
TextScroll: sw,
text: text,
buffer: buf,
@ -102,6 +128,13 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
text.SetFocusVAdjustment(sw.GetVAdjustment())
text.Connect("key-press-event", field.keyDown)
// // Connect to the field's revealer. On resize, we want the autocompleter to
// // have the right padding too.
// f.username.Connect("size-allocate", func(w gtk.IWidget) {
// // Set the autocompleter's left margin to be the same.
// c.SetMarginStart(w.ToWidget().GetAllocatedWidth())
// })
return field
}
@ -113,7 +146,7 @@ func (f *Field) Reset() {
f.UserID = ""
f.Sender = nil
f.editor = nil
f.username.Reset()
f.Username.Reset()
// reset the input
f.buffer.Delete(f.buffer.GetBounds())
@ -123,7 +156,7 @@ func (f *Field) Reset() {
// disabled. Reset() should be called first.
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
// Update the left username container in the input.
f.username.Update(session, sender)
f.Username.Update(session, sender)
f.UserID = session.ID()
// Set the sender.

View File

@ -58,9 +58,9 @@ func (f *Field) sendInput() {
f.SendMessage(SendMessageData{
time: time.Now().UTC(),
content: text,
author: f.username.GetLabel(),
author: f.Username.GetLabel(),
authorID: f.UserID,
authorURL: f.username.GetIconURL(),
authorURL: f.Username.GetIconURL(),
nonce: f.generateNonce(),
})
}

View File

@ -1,9 +1,10 @@
package input
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/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
@ -23,7 +24,7 @@ func init() {
))
}
type usernameContainer struct {
type Container struct {
*gtk.Revealer
main *gtk.Box
avatar *rich.Icon
@ -31,11 +32,17 @@ type usernameContainer struct {
}
var (
_ cchat.LabelContainer = (*usernameContainer)(nil)
_ cchat.IconContainer = (*usernameContainer)(nil)
_ cchat.LabelContainer = (*Container)(nil)
_ cchat.IconContainer = (*Container)(nil)
)
func newUsernameContainer() *usernameContainer {
var usernameCSS = primitives.PrepareCSS(`
.username-view {
margin: 8px 10px;
}
`)
func NewContainer() *Container {
avatar := rich.NewIcon(AvatarSize, imgutil.Round(true))
avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
avatar.Show()
@ -47,13 +54,12 @@ func newUsernameContainer() *usernameContainer {
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
box.PackStart(avatar, false, false, 0)
box.PackStart(label, false, false, 0)
box.SetMarginStart(10)
box.SetMarginEnd(10)
box.SetMarginTop(inputmargin)
box.SetMarginBottom(inputmargin)
box.SetVAlign(gtk.ALIGN_START)
box.Show()
primitives.AddClass(box, "username-view")
primitives.AttachCSS(box, usernameCSS)
rev, _ := gtk.RevealerNew()
rev.SetRevealChild(false)
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
@ -65,7 +71,7 @@ func newUsernameContainer() *usernameContainer {
// thread.
currentRevealer = rev.SetRevealChild
return &usernameContainer{
return &Container{
Revealer: rev,
main: box,
avatar: avatar,
@ -73,24 +79,24 @@ func newUsernameContainer() *usernameContainer {
}
}
func (u *usernameContainer) SetRevealChild(reveal bool) {
func (u *Container) SetRevealChild(reveal bool) {
// Only reveal if showUser is true.
u.Revealer.SetRevealChild(reveal && showUser)
}
// shouldReveal returns whether or not the container should reveal.
func (u *usernameContainer) shouldReveal() bool {
func (u *Container) shouldReveal() bool {
return !u.label.GetLabel().Empty() && showUser
}
func (u *usernameContainer) Reset() {
func (u *Container) Reset() {
u.SetRevealChild(false)
u.avatar.Reset()
u.label.Reset()
}
// Update is not thread-safe.
func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMessageSender) {
func (u *Container) Update(session cchat.Session, sender cchat.ServerMessageSender) {
// Set the fallback username.
u.label.SetLabelUnsafe(session.Name())
// Reveal the name if it's not empty.
@ -108,12 +114,12 @@ func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMes
}
// GetLabel is not thread-safe.
func (u *usernameContainer) GetLabel() text.Rich {
func (u *Container) GetLabel() text.Rich {
return u.label.GetLabel()
}
// SetLabel is thread-safe.
func (u *usernameContainer) SetLabel(content text.Rich) {
func (u *Container) SetLabel(content text.Rich) {
gts.ExecAsync(func() {
u.label.SetLabelUnsafe(content)
@ -123,7 +129,7 @@ func (u *usernameContainer) SetLabel(content text.Rich) {
}
// SetIcon is thread-safe.
func (u *usernameContainer) SetIcon(url string) {
func (u *Container) SetIcon(url string) {
gts.ExecAsync(func() {
u.avatar.SetIconUnsafe(url)
@ -136,6 +142,6 @@ func (u *usernameContainer) SetIcon(url string) {
}
// GetIconURL is not thread-safe.
func (u *usernameContainer) GetIconURL() string {
func (u *Container) GetIconURL() string {
return u.avatar.URL()
}

View File

@ -81,7 +81,7 @@ func NewEmptyContainer() *GenericContainer {
ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
ts.SetXAlign(1) // right align
ts.SetVAlign(gtk.ALIGN_END)
ts.SetSelectable(true)
// ts.SetSelectable(true)
ts.Show()
user, _ := gtk.LabelNew("")
@ -90,7 +90,7 @@ func NewEmptyContainer() *GenericContainer {
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
user.SetXAlign(1) // right align
user.SetVAlign(gtk.ALIGN_START)
user.SetSelectable(true)
// user.SetSelectable(true)
user.Show()
content, _ := gtk.LabelNew("")

View File

@ -0,0 +1,56 @@
package typing
import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gtk"
)
var dotsCSS = primitives.PrepareCSS(`
@keyframes breathing {
0% { opacity: 0.66; }
100% { opacity: 0.12; }
}
label {
animation: breathing 800ms infinite alternate;
}
label:nth-child(1) {
animation-delay: 000ms;
}
label:nth-child(2) {
animation-delay: 150ms;
}
label:nth-child(3) {
animation-delay: 300ms;
}
`)
const breathingChar = "●"
func NewDots() *gtk.Box {
c1, _ := gtk.LabelNew(breathingChar)
c1.Show()
c2, _ := gtk.LabelNew(breathingChar)
c2.Show()
c3, _ := gtk.LabelNew(breathingChar)
c3.Show()
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
b.Add(c1)
b.Add(c2)
b.Add(c3)
primitives.AddClass(b, "breathing-dots")
primitives.AttachCSS(c1, dotsCSS)
primitives.AttachCSS(c1, smallfonts)
primitives.AttachCSS(c2, dotsCSS)
primitives.AttachCSS(c2, smallfonts)
primitives.AttachCSS(c3, dotsCSS)
primitives.AttachCSS(c3, smallfonts)
return b
}

View File

@ -0,0 +1,145 @@
package typing
import (
"sort"
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/pkg/errors"
)
type State struct {
// states
typers []cchat.Typer
timeout time.Duration
canceler func()
invalidated bool
// consts
changed func(s *State, empty bool)
stopper func() // stops the event loop, not used atm
}
var _ cchat.TypingIndicator = (*State)(nil)
func NewState(changed func(s *State, empty bool)) *State {
s := &State{changed: changed}
s.stopper = gts.AfterFunc(time.Second/2, s.loop)
return s
}
func (s *State) reset() {
if s.canceler != nil {
s.canceler()
s.canceler = nil
}
s.timeout = 0
s.typers = nil
s.invalidated = false
}
// Subscribe is thread-safe.
func (s *State) Subscribe(indicator cchat.ServerMessageTypingIndicator) {
gts.Async(func() (func(), error) {
c, err := indicator.TypingSubscribe(s)
if err != nil {
return nil, errors.Wrap(err, "Failed to subscribe to typing indicator")
}
return func() {
s.canceler = c
s.timeout = indicator.TypingTimeout()
}, nil
})
}
// loop runs a single iteration of the event loop. This function is not
// thread-safe.
func (s *State) loop() {
// Filter out any expired typers.
t, ok := filterTypers(s.typers, s.timeout)
if ok {
s.invalidated = true
s.typers = t
}
// Call the event handler if things are invalidated.
if s.invalidated {
s.changed(s, len(s.typers) == 0)
s.invalidated = false
}
}
// 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())
})
s.invalidated = true
}
// AddTyper is thread-safe.
func (s *State) AddTyper(typer cchat.Typer) {
gts.ExecAsync(func() {
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() {
s.typers[i] = t
return
}
}
s.typers = append(s.typers, typer)
})
}
// RemoveTyper is thread-safe.
func (s *State) RemoveTyper(typerID string) {
gts.ExecAsync(func() { s.removeTyper(typerID) })
}
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
}
}
}
func filterTypers(typers []cchat.Typer, timeout time.Duration) ([]cchat.Typer, bool) {
// Fast path.
if len(typers) == 0 || timeout == 0 {
return nil, false
}
var now = time.Now()
var cut int
for _, t := range typers {
if now.Sub(t.Time()) < timeout {
typers[cut] = t
cut++
}
}
for i := cut; i < len(typers); i++ {
typers[i] = nil
}
var changed = cut != len(typers)
return typers[:cut], changed
}

View File

@ -0,0 +1,118 @@
package typing
import (
"strings"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
var typingIndicatorCSS = primitives.PrepareCSS(`
.typing-indicator {
margin: 0 6px;
border-radius: 6px 6px 0 0;
color: alpha(@theme_fg_color, 0.8);
background-color: @theme_base_color;
}
`)
var smallfonts = primitives.PrepareCSS(`
* { font-size: 0.9em; }
`)
type Container struct {
*gtk.Revealer
state *State
}
func New() *Container {
d := NewDots()
d.Show()
l, _ := gtk.LabelNew("")
l.SetXAlign(0)
l.SetEllipsize(pango.ELLIPSIZE_END)
l.Show()
primitives.AttachCSS(l, smallfonts)
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
b.PackStart(d, false, false, 4)
b.PackStart(l, true, true, 0)
b.Show()
r, _ := gtk.RevealerNew()
r.SetTransitionDuration(50)
r.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE)
r.SetRevealChild(false)
r.Add(b)
primitives.AddClass(b, "typing-indicator")
primitives.AttachCSS(b, typingIndicatorCSS)
state := NewState(func(s *State, empty bool) {
r.SetRevealChild(!empty)
l.SetMarkup(render(s.typers))
})
// On label destroy, stop the state loop as well.
l.Connect("destroy", state.stopper)
return &Container{
Revealer: r,
state: state,
}
}
func (c *Container) Reset() {
c.state.reset()
c.SetRevealChild(false)
}
func (c *Container) RemoveAuthor(author cchat.MessageAuthor) {
c.state.removeTyper(author.ID())
}
func (c *Container) TrySubscribe(svmsg cchat.ServerMessage) bool {
ti, ok := svmsg.(cchat.ServerMessageTypingIndicator)
if !ok {
return false
}
c.state.Subscribe(ti)
return true
}
func render(typers []cchat.Typer) string {
// fast path
if len(typers) == 0 {
return ""
}
var builder strings.Builder
for i, typer := range typers {
builder.WriteString("<b>")
builder.WriteString(parser.RenderMarkup(typer.Name()))
builder.WriteString("</b>")
switch i {
case len(typers) - 2:
builder.WriteString(" and ")
case len(typers) - 1:
// Write nothing if this is the last item.
default:
builder.WriteString(", ")
}
}
if len(typers) == 1 {
builder.WriteString(" is typing.")
} else {
builder.WriteString(" are typing.")
}
return builder.String()
}

View File

@ -13,6 +13,9 @@ 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/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/service/menu"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
@ -37,7 +40,11 @@ type View struct {
*sadface.FaceView
Box *gtk.Box
Scroller *autoscroll.ScrolledWindow
InputView *input.InputView
MsgBox *gtk.Box
Typing *typing.Container
Container container.Container
contType int // msgIndex
@ -47,16 +54,36 @@ type View struct {
func NewView() *View {
view := &View{}
view.InputView = input.NewView(view)
view.Typing = typing.New()
view.Typing.Show()
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
view.Box.PackEnd(view.InputView, false, false, 0)
view.Box.Show()
view.MsgBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
view.MsgBox.PackEnd(view.Typing, false, false, 0)
view.MsgBox.Show()
// Create the message container, which will use PackEnd to add the widget on
// TOP of the input view.
// TOP of the typing indicator.
view.createMessageContainer()
view.Scroller = autoscroll.NewScrolledWindow()
view.Scroller.Add(view.MsgBox)
view.Scroller.Show()
// A separator to go inbetween.
sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
sep.Show()
view.InputView = input.NewView(view)
view.InputView.Show()
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
view.Box.PackStart(view.Scroller, true, true, 0)
view.Box.PackStart(sep, false, false, 0)
view.Box.PackStart(view.InputView, false, false, 0)
view.Box.Show()
primitives.AddClass(view.Box, "message-view")
// placeholder logo
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256())
logo.Show()
@ -68,7 +95,7 @@ func NewView() *View {
func (v *View) createMessageContainer() {
// Remove the old message container.
if v.Container != nil {
v.Box.Remove(v.Container)
v.MsgBox.Remove(v.Container)
}
// Update the container type.
@ -80,15 +107,21 @@ func (v *View) createMessageContainer() {
}
// Add the new message container.
v.Box.PackEnd(v.Container, true, true, 0)
v.MsgBox.PackEnd(v.Container, true, true, 0)
}
func (v *View) Bottomed() bool { return v.Scroller.Bottomed }
func (v *View) Reset() {
v.state.Reset() // Reset the state variables.
v.Typing.Reset() // Reset the typing state.
v.FaceView.Reset() // Switch back to the main screen.
v.InputView.Reset() // Reset the input.
v.Container.Reset() // Clean all messages.
// Keep the scroller at the bottom.
v.Scroller.Bottomed = true
// Recreate the message container if the type is different.
if v.contType != msgIndex {
v.createMessageContainer()
@ -135,6 +168,8 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func
// Set the cancel handler.
v.state.setcurrent(s)
// Try setting the typing indicator if available.
v.Typing.TrySubscribe(server)
}, nil
})
}
@ -156,6 +191,11 @@ func (v *View) AddPresendMessage(msg input.PresendMessage) func(error) {
}
}
// AuthorEvent should be called on message create/update/delete.
func (v *View) AuthorEvent(author cchat.MessageAuthor) {
v.Typing.RemoveAuthor(author)
}
// LatestMessageFrom returns the last message ID with that author.
func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) {
return v.Container.LatestMessageFrom(userID)

View File

@ -48,7 +48,7 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
input.Connect("key-press-event", KeyDownHandler(l, input.GrabFocus))
ibuf, _ := input.GetBuffer()
ibuf.Connect("changed", func() {
ibuf.Connect("end-user-action", func() {
t, v := State(ibuf)
c.Cursor = v
c.Words, c.Index = split.SpaceIndexed(t, v)

108
internal/ui/rich/async.go Normal file
View File

@ -0,0 +1,108 @@
package rich
import (
"context"
"log"
"reflect"
"time"
"github.com/diamondburned/cchat-gtk/internal/gts"
)
// Reuser is an interface for structs that inherit Reusable.
type Reuser interface {
Context() context.Context
Acquire() int64
Validate(int64) bool
SwapResource(v interface{}, cancel func())
}
type AsyncUser = func(context.Context) (interface{}, func(), error)
// AsyncUse is a handler for structs that implement the Reuser primitive. The
// passed in function will be called asynchronously, but swap will be called in
// the Gtk main thread.
func AsyncUse(r Reuser, fn AsyncUser) {
// Acquire an ID.
id := r.Acquire()
ctx := r.Context()
gts.Async(func() (func(), error) {
// Run the callback asynchronously.
v, cancel, err := fn(ctx)
if err != nil {
return nil, err
}
return func() {
// Validate the ID. Cancel if it's invalid.
if !r.Validate(id) {
log.Println("Async function value dropped for reusable primitive.")
return
}
// Update the resource.
r.SwapResource(v, cancel)
}, nil
})
}
// Reusable is the synchronization primitive to provide a method for
// asynchronous cancellation and reusability.
//
// It works by copying the ID (time) for each asynchronous operation. The
// operation then completes, and the ID is then compared again before being
// used. It provides a cancellation abstraction around the Gtk main thread.
//
// This struct is not thread-safe, as it relies on the Gtk main thread
// synchronization.
type Reusable struct {
time int64 // creation time, used as ID
ctx context.Context
cancel func()
swapfn reflect.Value // reflect fn
arg1type reflect.Type
}
var _ Reuser = (*Reusable)(nil)
func NewReusable(swapperFn interface{}) *Reusable {
r := Reusable{}
r.swapfn = reflect.ValueOf(swapperFn)
r.arg1type = r.swapfn.Type().In(0)
r.Invalidate()
return &r
}
// Invalidate generates a new ID for the primitive, which would render
// asynchronously updating elements invalid.
func (r *Reusable) Invalidate() {
// Cancel the old context.
if r.cancel != nil {
r.cancel()
}
// Reset.
r.time = time.Now().UnixNano()
r.ctx, r.cancel = context.WithCancel(context.Background())
}
// Context returns the reusable's cancellable context. It never returns nil.
func (r *Reusable) Context() context.Context {
return r.ctx
}
// Reusable checks the acquired ID against the current one.
func (r *Reusable) Validate(acquired int64) (valid bool) {
return r.time == acquired
}
// Acquire lends the ID to be given to Reusable() after finishing.
func (r *Reusable) Acquire() int64 {
return r.time
}
func (r *Reusable) SwapResource(v interface{}, cancel func()) {
r.swapfn.Call([]reflect.Value{reflect.ValueOf(v)})
}

View File

@ -16,12 +16,11 @@ type IconerFn = func(context.Context, cchat.IconContainer) (func(), error)
type Icon struct {
*gtk.Revealer
Image *gtk.Image
resizer imgutil.Processor
procs []imgutil.Processor
size int
Image *gtk.Image
procs []imgutil.Processor
size int
r gts.Reusable
r *Reusable
// state
url string
@ -50,10 +49,11 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
Revealer: rev,
Image: img,
procs: procs,
r: *gts.NewReusable(),
}
i.SetSize(sizepx)
i.r = NewReusable(func(ni *nullIcon) {
i.SetIconUnsafe(ni.url)
})
return i
}
@ -61,6 +61,7 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
// Reset wipes the state to be just after construction.
func (i *Icon) Reset() {
i.url = ""
i.r.Invalidate() // invalidate async fetching images
i.Revealer.SetRevealChild(false)
i.Image.SetFromPixbuf(nil) // destroy old pb
}
@ -99,7 +100,6 @@ func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
func (i *Icon) SetSize(szpx int) {
i.size = szpx
i.Image.SetSizeRequest(szpx, szpx)
i.resizer = imgutil.Resize(szpx, szpx)
}
// AddProcessors is not thread-safe.
@ -112,16 +112,11 @@ func (i *Icon) SetIcon(url string) {
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
}
func (i *Icon) swapResource(v interface{}) {
i.SetIconUnsafe(v.(*nullIcon).url)
}
func (i *Icon) AsyncSetIconer(iconer cchat.Icon, wrap string) {
gts.AsyncUse(&i.r, i.swapResource, func(ctx context.Context) (interface{}, error) {
AsyncUse(i.r, func(ctx context.Context) (interface{}, func(), error) {
ni := &nullIcon{}
f, err := iconer.Icon(ctx, ni)
ni.cancel = f
return ni, err
return ni, f, err
})
}
@ -133,7 +128,11 @@ func (i *Icon) SetIconUnsafe(url string) {
}
func (i *Icon) updateAsync() {
httputil.AsyncImage(i.Image, i.url, imgutil.Prepend(i.resizer, i.procs)...)
if i.size > 0 {
httputil.AsyncImageSized(i.Image, i.url, i.size, i.size, i.procs...)
} else {
httputil.AsyncImage(i.Image, i.url, i.procs...)
}
}
type ToggleButtonImage struct {

View File

@ -30,7 +30,7 @@ type Label struct {
current text.Rich
// Reusable primitive.
r gts.Reusable
r *Reusable
}
var (
@ -44,12 +44,17 @@ func NewLabel(content text.Rich) *Label {
label.SetXAlign(0) // left align
label.SetEllipsize(pango.ELLIPSIZE_END)
return &Label{
l := &Label{
Label: *label,
current: content,
// reusable primitive, take reference
r: *gts.NewReusable(),
}
// reusable primitive
l.r = NewReusable(func(nl *nullLabel) {
l.SetLabelUnsafe(nl.Rich)
})
return l
}
// Reset wipes the state to be just after construction.
@ -59,17 +64,11 @@ func (l *Label) Reset() {
l.Label.SetText("")
}
// swapResource is reserved for internal use only.
func (l *Label) swapResource(v interface{}) {
l.SetLabelUnsafe(v.(*nullLabel).Rich)
}
func (l *Label) AsyncSetLabel(fn LabelerFn, info string) {
gts.AsyncUse(&l.r, l.swapResource, func(ctx context.Context) (interface{}, error) {
AsyncUse(l.r, func(ctx context.Context) (interface{}, func(), error) {
nl := &nullLabel{}
f, err := fn(ctx, nl)
nl.cancel = f
return nl, err
return nl, f, err
})
}

View File

@ -17,15 +17,13 @@ func MakeRed(content text.Rich) string {
// used for grabbing text without changing state
type nullLabel struct {
text.Rich
cancel func()
}
func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t }
// used for grabbing url without changing state
type nullIcon struct {
url string
cancel func()
url string
}
func (i *nullIcon) SetIcon(url string) { i.url = url }

View File

@ -21,11 +21,18 @@ type View struct {
Services []*Container
}
var servicesCSS = primitives.PrepareCSS(`
.services {
background-color: @theme_base_color;
}
`)
func NewView() *View {
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.Show()
primitives.AddClass(box, "services")
primitives.AttachCSS(box, servicesCSS)
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)

View File

@ -24,6 +24,10 @@ var monospace = primitives.PrepareCSS(`
}
`)
var commandPadding = primitives.PrepareCSS(`
* { padding: 8px 12px; }
`)
type Session struct {
*gtk.Box
@ -101,6 +105,8 @@ func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
primitives.AddClass(b, "commander")
primitives.AddClass(view, "command-buffer")
primitives.AddClass(input, "command-input")
primitives.AttachCSS(view, commandPadding)
primitives.AttachCSS(input, commandPadding)
return session
}

View File

@ -1,17 +1,10 @@
/* Make CSS more consistent across themes */
headerbar { padding: 0; }
/* Consistent design */
.services button { border-radius: 0; }
.message-content, .message-content text {
background: none;
box-shadow: none;
border: none;
}
popover > box { margin: 6px; }
popover > box {
margin: 6px;
}
.command-buffer, .command-input {
padding: 8px 12px;
}
/* Hack to fix the input bar being high in Adwaita */
.input-field * { min-height: 0; }