Photo sharing support
This commit is contained in:
parent
a23f5080d5
commit
d819a1181d
14
go.mod
14
go.mod
|
@ -5,18 +5,24 @@ go 1.19
|
|||
require (
|
||||
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b
|
||||
github.com/BurntSushi/toml v1.2.1
|
||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3
|
||||
github.com/microcosm-cc/bluemonday v1.0.22
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629
|
||||
golang.org/x/image v0.6.0
|
||||
nhooyr.io/websocket v1.8.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.10.3 // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||
github.com/sergi/go-diff v1.3.1 // indirect
|
||||
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect
|
||||
github.com/shurcooL/go-goon v1.0.0 // indirect
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480 // indirect
|
||||
github.com/shurcooL/highlight_go v0.0.0-20191220051317-782971ddf21b // indirect
|
||||
github.com/shurcooL/octicon v0.0.0-20191102190552-cbb32d6a785c // indirect
|
||||
|
@ -24,8 +30,8 @@ require (
|
|||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
|
||||
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e // indirect
|
||||
golang.org/x/crypto v0.5.0 // indirect
|
||||
golang.org/x/net v0.5.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/term v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
)
|
||||
|
|
84
go.sum
84
go.sum
|
@ -5,44 +5,74 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
|
|||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
|
||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/microcosm-cc/bluemonday v1.0.22 h1:p2tT7RNzRdCi0qmwxG+HbqD6ILkmwter1ZwVZn1oTxA=
|
||||
github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
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.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629 h1:86e54L0i3pH3dAIA8OxBbfLrVyhoGpnNk1iJCigAWYs=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ=
|
||||
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v1.0.0 h1:BCQPvxGkHHJ4WpBO4m/9FXbITVIsvAm/T66cCcCGI7E=
|
||||
github.com/shurcooL/go-goon v1.0.0/go.mod h1:2wTHMsGo7qnpmqA8ADYZtP4I1DD94JpXGQ3Dxq2YQ5w=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480 h1:KaKXZldeYH73dpQL+Nr38j1r5BgpAYQjYvENOUpIZDQ=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20191220051317-782971ddf21b h1:rBIwpb5ggtqf0uZZY5BPs1sL7njUMM7I8qD2jiou70E=
|
||||
|
@ -57,28 +87,66 @@ github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG
|
|||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e h1:Ee+VZw13r9NTOMnwTPs6O5KZ0MJU54hsxu9FpZ4pQ10=
|
||||
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e/go.mod h1:fSIW/szJHsRts/4U8wlMPhs+YqJC+7NYR+Qqb1uJVpA=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
// Version of the config format - when new fields are added, it will attempt
|
||||
// to write the settings.toml to disk so new defaults populate.
|
||||
var currentVersion = 3
|
||||
var currentVersion = 2
|
||||
|
||||
// Config for your BareRTC app.
|
||||
type Config struct {
|
||||
|
@ -33,6 +33,10 @@ type Config struct {
|
|||
|
||||
UseXForwardedFor bool
|
||||
|
||||
WebSocketReadLimit int64
|
||||
MaxImageWidth int
|
||||
PreviewImageWidth int
|
||||
|
||||
PublicChannels []Channel
|
||||
}
|
||||
|
||||
|
@ -65,6 +69,9 @@ func DefaultConfig() Config {
|
|||
CORSHosts: []string{
|
||||
"https://www.example.com",
|
||||
},
|
||||
WebSocketReadLimit: 1024 * 1024 * 40, // 40 MB.
|
||||
MaxImageWidth: 1280,
|
||||
PreviewImageWidth: 360,
|
||||
PublicChannels: []Channel{
|
||||
{
|
||||
ID: "lobby",
|
||||
|
@ -104,7 +111,7 @@ func LoadSettings() error {
|
|||
_, err = toml.Decode(string(data), &Current)
|
||||
|
||||
// Have we added new config fields? Save the settings.toml.
|
||||
if Current.Version < currentVersion {
|
||||
if Current.Version != currentVersion {
|
||||
log.Warn("New options are available for your settings.toml file. Your settings will be re-saved now.")
|
||||
Current.Version = currentVersion
|
||||
if err := WriteSettings(); err != nil {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package barertc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -141,6 +143,64 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
|||
s.Broadcast(message)
|
||||
}
|
||||
|
||||
// OnFile handles a picture shared in chat with a channel.
|
||||
func (s *Server) OnFile(sub *Subscriber, msg Message) {
|
||||
if sub.Username == "" {
|
||||
sub.ChatServer("You must log in first.")
|
||||
return
|
||||
}
|
||||
|
||||
// Detect image type and convert it into an <img src="data:"> tag.
|
||||
var (
|
||||
filename = msg.Message
|
||||
ext = filepath.Ext(filename)
|
||||
filetype string
|
||||
)
|
||||
switch strings.ToLower(ext) {
|
||||
case ".jpg", ".jpeg":
|
||||
filetype = "image/jpeg"
|
||||
case ".gif":
|
||||
filetype = "image/gif"
|
||||
case ".png":
|
||||
filetype = "image/png"
|
||||
default:
|
||||
sub.ChatServer("Unsupported image type, should be a jpeg, GIF or png.")
|
||||
return
|
||||
}
|
||||
|
||||
// Process the image: scale it down, strip metadata, etc.
|
||||
img, pvWidth, pvHeight := ProcessImage(filetype, msg.Bytes)
|
||||
var dataURL = fmt.Sprintf("data:%s;base64,%s", filetype, base64.StdEncoding.EncodeToString(img))
|
||||
|
||||
// Message to be echoed to the channel.
|
||||
var message = Message{
|
||||
Action: ActionMessage,
|
||||
Channel: msg.Channel,
|
||||
Username: sub.Username,
|
||||
|
||||
// Their image embedded via a data: URI - no server storage needed!
|
||||
Message: fmt.Sprintf(
|
||||
`<img src="%s" width="%d" height="%d" onclick="setModalImage(this.src)" style="cursor: pointer">`,
|
||||
dataURL,
|
||||
pvWidth, pvHeight,
|
||||
),
|
||||
}
|
||||
|
||||
// Is this a DM?
|
||||
if strings.HasPrefix(msg.Channel, "@") {
|
||||
// Echo the message only to both parties.
|
||||
s.SendTo(sub.Username, message)
|
||||
message.Channel = "@" + sub.Username
|
||||
if err := s.SendTo(msg.Channel, message); err != nil {
|
||||
sub.ChatServer("Your message could not be delivered: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Broadcast a chat message to the room.
|
||||
s.Broadcast(message)
|
||||
}
|
||||
|
||||
// OnMe handles current user state updates.
|
||||
func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
||||
if msg.VideoActive {
|
||||
|
|
122
pkg/images.go
Normal file
122
pkg/images.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package barertc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
|
||||
"git.kirsle.net/apps/barertc/pkg/config"
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
"github.com/edwvee/exiffix"
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO: configurable
|
||||
MaxPhotoWidth = 1280
|
||||
)
|
||||
|
||||
// ProcessImage treats user uploaded images:
|
||||
//
|
||||
// - Scales them down to a reasonable size
|
||||
// - Strips EXIF metadata
|
||||
//
|
||||
// and returns the modified image again as bytes.
|
||||
//
|
||||
// Also returns the suggested preview width, height to draw the image
|
||||
// at. This may be smaller than its true width x height.
|
||||
//
|
||||
// Filetype should be image/jpeg, image/gif or image/png.
|
||||
func ProcessImage(fileType string, data []byte) ([]byte, int, int) {
|
||||
reader := bytes.NewReader(data)
|
||||
|
||||
// Strip EXIF data.
|
||||
origImage, _, err := exiffix.Decode(reader)
|
||||
if err != nil {
|
||||
log.Error("ProcessImage: exiffix: %s", err)
|
||||
return data, config.Current.PreviewImageWidth, config.Current.PreviewImageWidth
|
||||
}
|
||||
|
||||
reader.Seek(0, io.SeekStart)
|
||||
var width, height = origImage.Bounds().Max.X, origImage.Bounds().Max.Y
|
||||
|
||||
log.Info("ProcessImage: taking a %dx%d image", width, height)
|
||||
|
||||
// Compute what size we should scale the width/height to,
|
||||
// and the even smaller preview size for front-end.
|
||||
var (
|
||||
previewWidth = config.Current.PreviewImageWidth
|
||||
previewHeight = previewWidth
|
||||
)
|
||||
if width >= height {
|
||||
log.Debug("Its width(%d) is >= its height (%d)", width, height)
|
||||
if width > config.Current.MaxImageWidth {
|
||||
newWidth := config.Current.MaxImageWidth
|
||||
log.Debug("\tnewWidth=%d", newWidth)
|
||||
log.Debug("\tnewHeight=(%d / %d) * %d", width, height, newWidth)
|
||||
height = int((float64(height) / float64(width)) * float64(newWidth))
|
||||
width = newWidth
|
||||
log.Debug("Its longest is width, scale to %dx%d", width, height)
|
||||
}
|
||||
|
||||
// Compute the preview width.
|
||||
if width > config.Current.PreviewImageWidth {
|
||||
newWidth := config.Current.PreviewImageWidth
|
||||
previewHeight = int((float64(height) / float64(width)) * float64(newWidth))
|
||||
previewWidth = newWidth
|
||||
}
|
||||
} else {
|
||||
if height > config.Current.MaxImageWidth {
|
||||
newHeight := config.Current.MaxImageWidth
|
||||
width = int((float64(width) / float64(height)) * float64(newHeight))
|
||||
height = newHeight
|
||||
log.Debug("Its longest is height, scale to %dx%d", width, height)
|
||||
}
|
||||
|
||||
// Compute the preview height.
|
||||
if height > config.Current.PreviewImageWidth {
|
||||
newHeight := config.Current.PreviewImageWidth
|
||||
previewWidth = int((float64(width) / float64(height)) * float64(newHeight))
|
||||
previewHeight = newHeight
|
||||
log.Debug("Its longest is height, scale to %dx%d", previewWidth, previewHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// Scale the image.
|
||||
scaledImg := Scale(origImage, image.Rect(0, 0, width, height), draw.ApproxBiLinear)
|
||||
|
||||
// Return the new bytes.
|
||||
var buf = bytes.NewBuffer([]byte{})
|
||||
switch fileType {
|
||||
case "image/jpeg":
|
||||
jpeg.Encode(buf, scaledImg, &jpeg.Options{
|
||||
Quality: 90,
|
||||
})
|
||||
case "image/gif":
|
||||
// Return the original data - we will only break it.
|
||||
return data, width, height
|
||||
case "image/png":
|
||||
png.Encode(buf, scaledImg)
|
||||
default:
|
||||
return data, config.Current.PreviewImageWidth, config.Current.PreviewImageWidth
|
||||
}
|
||||
|
||||
return buf.Bytes(), previewWidth, previewHeight
|
||||
}
|
||||
|
||||
// Scale down an image. Example:
|
||||
//
|
||||
// scaled := Scale(src, image.Rect(0, 0, 200, 200), draw.ApproxBiLinear)
|
||||
func Scale(src image.Image, rect image.Rectangle, scale draw.Scaler) image.Image {
|
||||
dst := image.NewRGBA(rect)
|
||||
copyRect := image.Rect(
|
||||
rect.Min.X,
|
||||
rect.Min.Y,
|
||||
rect.Min.X+rect.Max.X,
|
||||
rect.Min.Y+rect.Max.Y,
|
||||
)
|
||||
scale.Scale(dst, copyRect, src, src.Bounds(), draw.Over, nil)
|
||||
return dst
|
||||
}
|
|
@ -1,5 +1,12 @@
|
|||
package barertc
|
||||
|
||||
/*
|
||||
Message is the basic carrier of WebSocket chat protocol actions.
|
||||
|
||||
Every message (client or server) has an Action and the rest of the
|
||||
fields may vary depending on the action. Many messages target (or carry)
|
||||
a Username, chat Channel and carry an arbitrary Message.
|
||||
*/
|
||||
type Message struct {
|
||||
Action string `json:"action,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
|
@ -19,7 +26,11 @@ type Message struct {
|
|||
// Sent on `open` actions along with the (other) Username.
|
||||
OpenSecret string `json:"openSecret,omitempty"`
|
||||
|
||||
// Parameters sent on WebRTC signaling messages.
|
||||
// Send on `file` actions, passing e.g. image data.
|
||||
Bytes []byte `json:"bytes,omitempty"`
|
||||
|
||||
// WebRTC negotiation messages: proxy their signaling messages
|
||||
// between the two users to negotiate peer connection.
|
||||
Candidate string `json:"candidate,omitempty"` // candidate
|
||||
Description string `json:"description,omitempty"` // sdp
|
||||
}
|
||||
|
@ -35,6 +46,7 @@ const (
|
|||
ActionRing = "ring" // receiver of a WebRTC open request
|
||||
ActionWatch = "watch" // user has received video and is watching you
|
||||
ActionUnwatch = "unwatch" // user has closed your video
|
||||
ActionFile = "file" // image sharing in chat
|
||||
|
||||
// Actions sent by server only
|
||||
ActionPing = "ping"
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/barertc/pkg/config"
|
||||
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
"git.kirsle.net/apps/barertc/pkg/util"
|
||||
|
@ -59,18 +60,23 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
|||
|
||||
// Read the user's posted message.
|
||||
var msg Message
|
||||
log.Debug("Read(%d=%s): %s", sub.ID, sub.Username, data)
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
log.Error("Read(%d=%s) Message error: %s", sub.ID, sub.Username, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.Action != ActionFile {
|
||||
log.Debug("Read(%d=%s): %s", sub.ID, sub.Username, data)
|
||||
}
|
||||
|
||||
// What action are they performing?
|
||||
switch msg.Action {
|
||||
case ActionLogin:
|
||||
s.OnLogin(sub, msg)
|
||||
case ActionMessage:
|
||||
s.OnMessage(sub, msg)
|
||||
case ActionFile:
|
||||
s.OnFile(sub, msg)
|
||||
case ActionMe:
|
||||
s.OnMe(sub, msg)
|
||||
case ActionOpen:
|
||||
|
@ -96,7 +102,7 @@ func (sub *Subscriber) SendJSON(v interface{}) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("SendJSON(%d=%s): %s", sub.ID, sub.Username, data)
|
||||
// log.Debug("SendJSON(%d=%s): %s", sub.ID, sub.Username, data)
|
||||
return sub.conn.Write(sub.ctx, websocket.MessageText, data)
|
||||
}
|
||||
|
||||
|
@ -118,7 +124,7 @@ func (sub *Subscriber) ChatServer(message string, v ...interface{}) {
|
|||
})
|
||||
}
|
||||
|
||||
// WebSocket handles the /ws websocket connection.
|
||||
// WebSocket handles the /ws websocket connection endpoint.
|
||||
func (s *Server) WebSocket() http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := util.IPAddress(r)
|
||||
|
@ -133,6 +139,7 @@ func (s *Server) WebSocket() http.HandlerFunc {
|
|||
defer c.Close(websocket.StatusInternalError, "the sky is falling")
|
||||
|
||||
log.Debug("WebSocket: %s has connected", ip)
|
||||
c.SetReadLimit(config.Current.WebSocketReadLimit)
|
||||
|
||||
// CloseRead starts a goroutine that will read from the connection
|
||||
// until it is closed.
|
||||
|
@ -235,7 +242,10 @@ func (s *Server) IterSubscribers(isLocked ...bool) []*Subscriber {
|
|||
|
||||
// Broadcast a message to the chat room.
|
||||
func (s *Server) Broadcast(msg Message) {
|
||||
log.Debug("Broadcast: %+v", msg)
|
||||
if len(msg.Message) < 1024 {
|
||||
log.Debug("Broadcast: %+v", msg)
|
||||
}
|
||||
|
||||
s.subscribersMu.RLock()
|
||||
defer s.subscribersMu.RUnlock()
|
||||
for _, sub := range s.IterSubscribers(true) {
|
||||
|
|
|
@ -7,6 +7,17 @@ const configuration = {
|
|||
}]
|
||||
};
|
||||
|
||||
const FileUploadMaxSize = 1024 * 1024 * 8; // 8 MB
|
||||
|
||||
|
||||
function setModalImage(url) {
|
||||
let $modalImg = document.querySelector("#modalImage"),
|
||||
$modal = document.querySelector("#photo-modal");
|
||||
$modalImg.src = url;
|
||||
$modal.classList.add("is-active");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const app = Vue.createApp({
|
||||
delimiters: ['[[', ']]'],
|
||||
|
@ -1072,6 +1083,52 @@ const app = Vue.createApp({
|
|||
return `${(hour)}:${minutes}:${seconds} ${ampm}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Image sharing in chat
|
||||
*/
|
||||
|
||||
// The image upload button handler.
|
||||
uploadFile() {
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = e => {
|
||||
let file = e.target.files[0];
|
||||
if (file.size > FileUploadMaxSize) {
|
||||
this.ChatClient(`Please share an image smaller than ${FileUploadMaxSize / 1024 / 1024} MB in size!`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ChatClient(`<em>Uploading file to chat: ${file.name} - ${file.size} bytes, ${file.type} format.</em>`);
|
||||
|
||||
// Get image file data.
|
||||
let reader = new FileReader();
|
||||
let rawData = new ArrayBuffer();
|
||||
reader.onload = e => {
|
||||
rawData = e.target.result;
|
||||
|
||||
let fileByteArray = [],
|
||||
u8array = new Uint8Array(rawData);
|
||||
for (let i = 0; i < u8array.length; i++) {
|
||||
fileByteArray.push(u8array[i]);
|
||||
}
|
||||
|
||||
let msg = JSON.stringify({
|
||||
action: "file",
|
||||
channel: this.channel,
|
||||
message: file.name,
|
||||
bytes: fileByteArray, //btoa(fileByteArray),
|
||||
});
|
||||
|
||||
// Send it to the chat server.
|
||||
this.ws.conn.send(msg);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sound effect concerns.
|
||||
*/
|
||||
|
|
|
@ -275,6 +275,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photo Detail Modal -->
|
||||
<div class="modal" id="photo-modal">
|
||||
<div class="modal-background" onclick="document.querySelector('#photo-modal').classList.remove('is-active')"></div>
|
||||
<div class="modal-content photo-modal">
|
||||
<div class="image is-fullwidth">
|
||||
<img id="modalImage">
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
|
||||
<!-- Top header panel -->
|
||||
|
@ -588,12 +599,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Footer Frame -->
|
||||
<div class="chat-footer">
|
||||
<div class="card">
|
||||
<div class="card-content p-2">
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="column pr-1">
|
||||
<form @submit.prevent="sendMessage()">
|
||||
<input type="text" class="input"
|
||||
v-model="message"
|
||||
|
@ -601,6 +613,12 @@
|
|||
@keydown="sendTypingNotification()">
|
||||
</form>
|
||||
</div>
|
||||
<div class="column pl-1 is-narrow">
|
||||
<button type="button" class="button"
|
||||
@click="uploadFile()">
|
||||
<i class="fa fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user