Improved the server avatar upload modal
parent
f0903db925
commit
c68c217eaf
|
@ -1,4 +1,7 @@
|
|||
# Changelog:
|
||||
* **24.03.21**
|
||||
- Improved the avatar upload modal (now way more intuitive)
|
||||
|
||||
* **23.03.21**
|
||||
- Made the permission editor popoutable
|
||||
- Now using SVG flags for higher quality.
|
||||
|
|
|
@ -3426,6 +3426,11 @@
|
|||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/crypto-js": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.0.1.tgz",
|
||||
"integrity": "sha512-6+OPzqhKX/cx5xh+yO8Cqg3u3alrkhoxhE5ZOdSEv0DOzJ13lwJ6laqGU0Kv6+XDMFmlnGId04LtY22PsFLQUw=="
|
||||
},
|
||||
"@types/dompurify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.0.1.tgz",
|
||||
|
@ -4412,6 +4417,17 @@
|
|||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"asn1.js": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||
"requires": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"asn1js": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.1.0.tgz",
|
||||
|
@ -5597,8 +5613,7 @@
|
|||
"base64-js": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
|
||||
},
|
||||
"batch": {
|
||||
"version": "0.6.1",
|
||||
|
@ -5873,6 +5888,94 @@
|
|||
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
||||
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
|
||||
},
|
||||
"browserify-aes": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
|
||||
"requires": {
|
||||
"buffer-xor": "^1.0.3",
|
||||
"cipher-base": "^1.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"evp_bytestokey": "^1.0.3",
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"browserify-cipher": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
|
||||
"integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
|
||||
"requires": {
|
||||
"browserify-aes": "^1.0.4",
|
||||
"browserify-des": "^1.0.0",
|
||||
"evp_bytestokey": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"browserify-des": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
|
||||
"integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
|
||||
"requires": {
|
||||
"cipher-base": "^1.0.1",
|
||||
"des.js": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"browserify-rsa": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz",
|
||||
"integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==",
|
||||
"requires": {
|
||||
"bn.js": "^5.0.0",
|
||||
"randombytes": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz",
|
||||
"integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"browserify-sign": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz",
|
||||
"integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==",
|
||||
"requires": {
|
||||
"bn.js": "^5.1.1",
|
||||
"browserify-rsa": "^4.0.1",
|
||||
"create-hash": "^1.2.0",
|
||||
"create-hmac": "^1.1.7",
|
||||
"elliptic": "^6.5.3",
|
||||
"inherits": "^2.0.4",
|
||||
"parse-asn1": "^5.1.5",
|
||||
"readable-stream": "^3.6.0",
|
||||
"safe-buffer": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz",
|
||||
"integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.13.0.tgz",
|
||||
|
@ -5885,6 +5988,15 @@
|
|||
"node-releases": "^1.1.58"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
|
@ -5915,6 +6027,11 @@
|
|||
"integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
|
||||
"dev": true
|
||||
},
|
||||
"buffer-xor": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
|
||||
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
|
@ -6203,6 +6320,15 @@
|
|||
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
|
||||
"dev": true
|
||||
},
|
||||
"cipher-base": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
|
||||
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"circular-dependency-plugin": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz",
|
||||
|
@ -6796,6 +6922,40 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"create-ecdh": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||
"integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
|
||||
"requires": {
|
||||
"bn.js": "^4.1.0",
|
||||
"elliptic": "^6.5.3"
|
||||
}
|
||||
},
|
||||
"create-hash": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
|
||||
"requires": {
|
||||
"cipher-base": "^1.0.1",
|
||||
"inherits": "^2.0.1",
|
||||
"md5.js": "^1.3.4",
|
||||
"ripemd160": "^2.0.1",
|
||||
"sha.js": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"create-hmac": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
|
||||
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
|
||||
"requires": {
|
||||
"cipher-base": "^1.0.3",
|
||||
"create-hash": "^1.1.0",
|
||||
"inherits": "^2.0.1",
|
||||
"ripemd160": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
|
||||
|
@ -6806,6 +6966,29 @@
|
|||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"crypto-browserify": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
|
||||
"integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
|
||||
"requires": {
|
||||
"browserify-cipher": "^1.0.0",
|
||||
"browserify-sign": "^4.0.0",
|
||||
"create-ecdh": "^4.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"create-hmac": "^1.1.0",
|
||||
"diffie-hellman": "^5.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"pbkdf2": "^3.0.3",
|
||||
"public-encrypt": "^4.0.0",
|
||||
"randombytes": "^2.0.0",
|
||||
"randomfill": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"crypto-js": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz",
|
||||
"integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg=="
|
||||
},
|
||||
"crypto-random-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||
|
@ -7707,6 +7890,16 @@
|
|||
"integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==",
|
||||
"dev": true
|
||||
},
|
||||
"diffie-hellman": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
||||
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
|
||||
"requires": {
|
||||
"bn.js": "^4.1.0",
|
||||
"miller-rabin": "^4.0.0",
|
||||
"randombytes": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
|
@ -8311,6 +8504,15 @@
|
|||
"original": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"evp_bytestokey": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
|
||||
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
|
||||
"requires": {
|
||||
"md5.js": "^1.3.4",
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz",
|
||||
|
@ -10850,6 +11052,33 @@
|
|||
"integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
|
||||
"dev": true
|
||||
},
|
||||
"hash-base": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
|
||||
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.6.0",
|
||||
"safe-buffer": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
|
@ -11336,6 +11565,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||
|
@ -12889,6 +13123,16 @@
|
|||
"integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==",
|
||||
"optional": true
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
|
||||
"requires": {
|
||||
"hash-base": "^3.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"mdn-data": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz",
|
||||
|
@ -13059,6 +13303,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"miller-rabin": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
|
||||
"integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
|
||||
"requires": {
|
||||
"bn.js": "^4.0.0",
|
||||
"brorand": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
|
@ -13979,6 +14232,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"parse-asn1": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
|
||||
"integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==",
|
||||
"requires": {
|
||||
"asn1.js": "^5.2.0",
|
||||
"browserify-aes": "^1.0.0",
|
||||
"evp_bytestokey": "^1.0.0",
|
||||
"pbkdf2": "^3.0.3",
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"parse-filepath": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
|
||||
|
@ -14137,6 +14402,18 @@
|
|||
"pinkie-promise": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pbkdf2": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
|
||||
"integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==",
|
||||
"requires": {
|
||||
"create-hash": "^1.1.2",
|
||||
"create-hmac": "^1.1.4",
|
||||
"ripemd160": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
|
@ -16572,6 +16849,19 @@
|
|||
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"public-encrypt": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
|
||||
"integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
|
||||
"requires": {
|
||||
"bn.js": "^4.1.0",
|
||||
"browserify-rsa": "^4.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"parse-asn1": "^5.0.0",
|
||||
"randombytes": "^2.0.1",
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
|
||||
|
@ -16687,11 +16977,19 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"randomfill": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
|
||||
"integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
|
||||
"requires": {
|
||||
"randombytes": "^2.0.5",
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
|
@ -17565,6 +17863,15 @@
|
|||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"ripemd160": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
|
||||
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
|
||||
"requires": {
|
||||
"hash-base": "^3.0.0",
|
||||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"rtcpeerconnection-shim": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
|
||||
|
@ -17595,8 +17902,7 @@
|
|||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.22.10",
|
||||
|
@ -18137,6 +18443,15 @@
|
|||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
|
||||
"dev": true
|
||||
},
|
||||
"sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"sha256": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sha256/-/sha256-0.2.0.tgz",
|
||||
|
@ -18599,6 +18914,27 @@
|
|||
"readable-stream": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"stream-browserify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
|
||||
"integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==",
|
||||
"requires": {
|
||||
"inherits": "~2.0.4",
|
||||
"readable-stream": "^3.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"stream-events": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
||||
|
|
14
package.json
14
package.json
|
@ -8,14 +8,11 @@
|
|||
"trgen": "node tools/trgen/index.js",
|
||||
"tsc": "tsc",
|
||||
"compile-scss": "sass loader/css/index.scss:loader/css/index.css",
|
||||
"start": "npm run compile-project-base && node file.js ndevelop",
|
||||
"start": "npm run compile-project-base && webpack serve --config webpack-web.config.js",
|
||||
"start-client": "npm run compile-project-base && webpack serve --config webpack-client.config.js",
|
||||
"build-web": "webpack --config webpack-web.config.js",
|
||||
"build-client": "webpack --config webpack-client.config.js",
|
||||
"webpack-web": "webpack --config webpack-web.config.js",
|
||||
"webpack-client": "webpack --config webpack-client.config.js",
|
||||
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js",
|
||||
"dev-server": "npm run compile-project-base && webpack serve --config webpack-web.config.js",
|
||||
"dev-server-client": "npm run compile-project-base && webpack serve --config webpack-client.config.js"
|
||||
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js"
|
||||
},
|
||||
"author": "TeaSpeak (WolverinDEV)",
|
||||
"license": "ISC",
|
||||
|
@ -100,7 +97,11 @@
|
|||
},
|
||||
"homepage": "https://www.teaspeak.de",
|
||||
"dependencies": {
|
||||
"@types/crypto-js": "^4.0.1",
|
||||
"broadcastchannel-polyfill": "^1.0.1",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"crypto-js": "^4.0.0",
|
||||
"detect-browser": "^5.2.0",
|
||||
"dompurify": "^2.0.8",
|
||||
"emoji-mart": "git+https://github.com/WolverinDEV/emoji-mart.git",
|
||||
|
@ -118,6 +119,7 @@
|
|||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sdp-transform": "^2.14.0",
|
||||
"simple-jsonp-promise": "^1.1.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"twemoji": "^13.0.0",
|
||||
"url-knife": "^3.1.3",
|
||||
"webcrypto-liner": "^1.2.4",
|
||||
|
|
|
@ -1009,87 +1009,6 @@ export class ConnectionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
update_avatar() {
|
||||
spawnAvatarUpload(data => {
|
||||
if(typeof(data) === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!data) {
|
||||
logInfo(LogCategory.CLIENT, tr("Deleting existing avatar"));
|
||||
this.serverConnection.send_command('ftdeletefile', {
|
||||
name: "/avatar_", /* delete own avatar */
|
||||
path: "",
|
||||
cid: 0
|
||||
}).then(() => {
|
||||
createInfoModal(tr("Avatar deleted"), tr("Avatar successfully deleted")).open();
|
||||
}).catch(error => {
|
||||
logError(LogCategory.GENERAL, tr("Failed to reset avatar flag: %o"), error);
|
||||
|
||||
let message;
|
||||
if(error instanceof CommandResult) {
|
||||
message = formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
}
|
||||
|
||||
if(!message) {
|
||||
message = formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details"));
|
||||
}
|
||||
|
||||
createErrorModal(tr("Failed to delete avatar"), message).open();
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
logInfo(LogCategory.CLIENT, tr("Uploading new avatar"));
|
||||
(async () => {
|
||||
const transfer = this.fileManager.initializeFileUpload({
|
||||
name: "/avatar",
|
||||
path: "",
|
||||
|
||||
channel: 0,
|
||||
channelPassword: undefined,
|
||||
|
||||
source: async () => await TransferProvider.provider().createBufferSource(data)
|
||||
});
|
||||
|
||||
await transfer.awaitFinished();
|
||||
|
||||
if(transfer.transferState() !== FileTransferState.FINISHED) {
|
||||
if(transfer.transferState() === FileTransferState.ERRORED) {
|
||||
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to upload clients avatar: %o"), transfer.currentError());
|
||||
createErrorModal(tr("Failed to upload avatar"), traj("Failed to upload avatar:{:br:}{0}", transfer.currentErrorMessage())).open();
|
||||
return;
|
||||
} else if(transfer.transferState() === FileTransferState.CANCELED) {
|
||||
createErrorModal(tr("Failed to upload avatar"), tr("Your avatar upload has been canceled.")).open();
|
||||
return;
|
||||
} else {
|
||||
createErrorModal(tr("Failed to upload avatar"), tr("Avatar upload finished with an unknown finished state.")).open();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.serverConnection.send_command('clientupdate', {
|
||||
client_flag_avatar: md5(new Uint8Array(data))
|
||||
});
|
||||
} catch(error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to update avatar flag: %o"), error);
|
||||
|
||||
let message;
|
||||
if(error instanceof CommandResult)
|
||||
message = formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
if(!message)
|
||||
message = formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details"));
|
||||
createErrorModal(tr("Failed to set avatar"), message).open();
|
||||
return;
|
||||
}
|
||||
|
||||
createInfoModal(tr("Avatar successfully uploaded"), tr("Your avatar has been uploaded successfully!")).open();
|
||||
})();
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async initializeWhisperSession(session: WhisperSession) : Promise<WhisperSessionInitializeData> {
|
||||
/* TODO: Try to load the clients unique via a clientgetuidfromclid */
|
||||
if(!session.getClientUniqueId())
|
||||
|
|
|
@ -2,11 +2,21 @@ export class Mutex<T> {
|
|||
private value: T;
|
||||
private taskExecuting = false;
|
||||
private taskQueue = [];
|
||||
private freeListener = [];
|
||||
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
isFree() : boolean {
|
||||
return !this.taskExecuting && this.taskQueue.length === 0;
|
||||
}
|
||||
|
||||
awaitFree() : Promise<void> {
|
||||
return new Promise<void>(resolve => this.freeListener.push(resolve));
|
||||
}
|
||||
|
||||
execute<R>(callback: (value: T, setValue: (newValue: T) => void) => R | Promise<R>) : Promise<R> {
|
||||
return new Promise<R>((resolve, reject) => {
|
||||
this.taskQueue.push(() => new Promise(taskResolve => {
|
||||
|
@ -53,10 +63,26 @@ export class Mutex<T> {
|
|||
const task = this.taskQueue.pop_front();
|
||||
if(typeof task === "undefined") {
|
||||
this.taskExecuting = false;
|
||||
this.triggerFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
this.taskExecuting = true;
|
||||
task().then(() => this.executeNextTask());
|
||||
}
|
||||
|
||||
private triggerFinished() {
|
||||
while(this.isFree()) {
|
||||
const listener = this.freeListener.pop_front();
|
||||
if(!listener) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,8 +57,9 @@ export abstract class ClientAvatar {
|
|||
}
|
||||
|
||||
public getTypedStateData<T extends AvatarState>(state: T) : AvatarStateData[T] {
|
||||
if(this.state !== state)
|
||||
if(this.state !== state) {
|
||||
throw "invalid avatar state";
|
||||
}
|
||||
|
||||
return this.stateData as any;
|
||||
}
|
||||
|
@ -79,11 +80,34 @@ export abstract class ClientAvatar {
|
|||
this.setState("errored", data);
|
||||
}
|
||||
|
||||
async awaitLoaded() {
|
||||
if(this.state !== "loading")
|
||||
return;
|
||||
async awaitLoaded() : Promise<true>;
|
||||
async awaitLoaded(timeout: number) : Promise<boolean>;
|
||||
async awaitLoaded(timeout?: number) : Promise<boolean> {
|
||||
if(this.state !== "loading") {
|
||||
return true;
|
||||
}
|
||||
|
||||
await new Promise(resolve => this.events.on("avatar_state_changed", event => event.newState !== "loading" && resolve()));
|
||||
await new Promise(resolve => {
|
||||
let timeoutId;
|
||||
const callback = success => {
|
||||
if(typeof timeoutId !== "undefined") {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
unregister();
|
||||
resolve(success);
|
||||
};
|
||||
|
||||
const unregister = this.events.on("avatar_state_changed", event => {
|
||||
if(event.newState !== "loading") {
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
|
||||
if(typeof timeout === "number") {
|
||||
timeoutId = setTimeout(() => callback(false), timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getState() : AvatarState {
|
||||
|
@ -99,8 +123,10 @@ export abstract class ClientAvatar {
|
|||
}
|
||||
|
||||
getAvatarUrl() {
|
||||
if(this.state === "loaded")
|
||||
if(this.state === "loaded") {
|
||||
return this.getTypedStateData("loaded").url || kDefaultAvatarImage;
|
||||
}
|
||||
|
||||
return kDefaultAvatarImage;
|
||||
}
|
||||
|
||||
|
|
|
@ -147,12 +147,17 @@ export class ImageCache {
|
|||
if(!cacheInstance) { return; }
|
||||
|
||||
const new_headers = new Headers();
|
||||
for(const key of value.headers.keys())
|
||||
for(const key of value.headers.keys()) {
|
||||
new_headers.set(key, value.headers.get(key));
|
||||
if(type)
|
||||
}
|
||||
|
||||
if(type) {
|
||||
new_headers.set("Content-type", type);
|
||||
for(const key of Object.keys(headers || {}))
|
||||
}
|
||||
|
||||
for(const key of Object.keys(headers || {})) {
|
||||
new_headers.set(key, headers[key]);
|
||||
}
|
||||
|
||||
await cacheInstance.put("https://_local_cache/cache_request_" + key, new Response(value.body, {
|
||||
headers: new_headers
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
/* Note: This will be included into the controller and renderer process */
|
||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import * as crypto from "crypto-js";
|
||||
|
||||
export type LocalAvatarInfo = {
|
||||
fileName: string,
|
||||
fileSize: number,
|
||||
fileHashMD5: string,
|
||||
|
||||
fileUploaded: number,
|
||||
fileModified: number,
|
||||
|
||||
contentType: string,
|
||||
|
||||
resourceUrl: string | undefined,
|
||||
}
|
||||
|
||||
export type LocalAvatarUpdateResult = {
|
||||
status: "success"
|
||||
} | {
|
||||
status: "error",
|
||||
reason: string
|
||||
} | {
|
||||
status: "cache-unavailable"
|
||||
};
|
||||
|
||||
export type LocalAvatarLoadResult<T> = {
|
||||
status: "success",
|
||||
result: T,
|
||||
} | {
|
||||
status: "error",
|
||||
reason: string
|
||||
} | {
|
||||
status: "cache-unavailable" | "empty-result"
|
||||
};
|
||||
|
||||
const kMaxAvatarSize = 16 * 1024 * 1024;
|
||||
|
||||
export type OwnAvatarMode = "uploading" | "server";
|
||||
export class OwnAvatarStorage {
|
||||
private openedCache: Cache | undefined;
|
||||
|
||||
private static generateRequestUrl(serverUniqueId: string, mode: OwnAvatarMode) : string {
|
||||
return "https://_local_avatar/" + serverUniqueId + "/" + mode;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if(!("caches" in window)) {
|
||||
/* Not available (may unsecure context?) */
|
||||
this.openedCache = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.openedCache = await caches.open("local-avatars");
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to open local avatar cache: %o"), error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadAvatarRequest(serverUniqueId: string, mode: OwnAvatarMode) : Promise<LocalAvatarLoadResult<Response>> {
|
||||
if(!this.openedCache) {
|
||||
return { status: "cache-unavailable" };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.openedCache.match(OwnAvatarStorage.generateRequestUrl(serverUniqueId, mode), {
|
||||
ignoreMethod: true,
|
||||
ignoreSearch: true,
|
||||
});
|
||||
|
||||
if(!response) {
|
||||
return { status: "empty-result" };
|
||||
}
|
||||
|
||||
return { status: "success", result: response };
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to query local avatar cache: %o"), error);
|
||||
return { status: "error", reason: tr("failed to query cache") };
|
||||
}
|
||||
}
|
||||
|
||||
async loadAvatarImage(serverUniqueId: string, mode: OwnAvatarMode) : Promise<LocalAvatarLoadResult<ArrayBuffer>> {
|
||||
const loadResult = await this.loadAvatarRequest(serverUniqueId, mode);
|
||||
if(loadResult.status !== "success") {
|
||||
return loadResult;
|
||||
}
|
||||
|
||||
try {
|
||||
return { status: "success", result: await loadResult.result.arrayBuffer() };
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to load avatar into a buffer: %o"), error);
|
||||
return { status: "error", reason: tr("failed to load avatar into a buffer") };
|
||||
}
|
||||
}
|
||||
|
||||
async loadAvatar(serverUniqueId: string, mode: OwnAvatarMode, createResourceUrl: boolean) : Promise<LocalAvatarLoadResult<LocalAvatarInfo>> {
|
||||
const loadResult = await this.loadAvatarRequest(serverUniqueId, mode);
|
||||
if(loadResult.status !== "success") {
|
||||
return loadResult;
|
||||
}
|
||||
|
||||
const headers = loadResult.result.headers;
|
||||
const contentType = headers.get("Content-Type");
|
||||
const avatarSize = parseInt(headers.get("Content-Length"));
|
||||
const avatarHash = headers.get("X-Avatar-Hash");
|
||||
const avatarName = headers.get("X-File-Name");
|
||||
const avatarDateModified = parseInt(headers.get("X-File-Date-Modified"));
|
||||
const avatarDateUploaded = parseInt(headers.get("X-File-Uploaded"));
|
||||
|
||||
if(!avatarHash) {
|
||||
return { status: "error", reason: tr("missing response header file hash") };
|
||||
}
|
||||
|
||||
if(!avatarName) {
|
||||
return { status: "error", reason: tr("missing response header file name") };
|
||||
}
|
||||
|
||||
if(isNaN(avatarSize)) {
|
||||
return { status: "error", reason: tr("missing/invalid response header file size") };
|
||||
}
|
||||
|
||||
if(isNaN(avatarDateModified)) {
|
||||
return { status: "error", reason: tr("missing/invalid response header file modify date") };
|
||||
}
|
||||
|
||||
if(isNaN(avatarDateUploaded)) {
|
||||
return { status: "error", reason: tr("missing/invalid response header file upload date") };
|
||||
}
|
||||
|
||||
let resourceUrl;
|
||||
if(createResourceUrl) {
|
||||
try {
|
||||
resourceUrl = URL.createObjectURL(await loadResult.result.blob());
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to create avatar resource url: %o"), error);
|
||||
return { status: "error", reason: tr("failed to generate resource url") };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
result: {
|
||||
contentType: contentType,
|
||||
|
||||
fileName: avatarName,
|
||||
fileSize: avatarSize,
|
||||
fileHashMD5: avatarHash,
|
||||
|
||||
fileModified: avatarDateModified,
|
||||
fileUploaded: avatarDateUploaded,
|
||||
|
||||
resourceUrl: resourceUrl,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async updateAvatar(serverUniqueId: string, mode: OwnAvatarMode, target: File) : Promise<LocalAvatarUpdateResult> {
|
||||
if(!this.openedCache) {
|
||||
return { status: "cache-unavailable" };
|
||||
}
|
||||
|
||||
if(target.size > kMaxAvatarSize) {
|
||||
return { status: "error", reason: tra("Image exceeds maximum software size of {} bytes", kMaxAvatarSize) };
|
||||
}
|
||||
|
||||
let md5Hash: string;
|
||||
try {
|
||||
const hasher = crypto.algo.MD5.create();
|
||||
await target.stream().pipeTo(new WritableStream({
|
||||
write(data) {
|
||||
hasher.update(crypto.lib.WordArray.create(data));
|
||||
}
|
||||
}));
|
||||
|
||||
md5Hash = hasher.finalize().toString(crypto.enc.Hex);
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to create avatar md5 hash: %o"), error);
|
||||
return { status: "error", reason: tr("failed to create md5 hash") };
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set("X-Avatar-Hash", md5Hash);
|
||||
headers.set("X-File-Name", target.name);
|
||||
headers.set("X-File-Date-Modified", target.lastModified.toString());
|
||||
headers.set("X-File-Uploaded", Date.now().toString());
|
||||
headers.set("Content-Type", target.type);
|
||||
headers.set("Content-Length", target.size.toString());
|
||||
|
||||
try {
|
||||
const response = new Response(target, { headers: headers });
|
||||
await this.openedCache.put("https://_local_avatar/" + serverUniqueId + "/" + mode, response);
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to save local avatar: %o"), error);
|
||||
return { status: "error", reason: tr("failed to save avatar to disk") };
|
||||
}
|
||||
|
||||
return { status: "success" };
|
||||
}
|
||||
|
||||
async removeAvatar(serverUniqueId: string, mode: OwnAvatarMode) {
|
||||
if(!this.openedCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.openedCache.delete(OwnAvatarStorage.generateRequestUrl(serverUniqueId, mode));
|
||||
} catch (error) {
|
||||
logWarn(LogCategory.GENERAL, tr("Failed to delete avatar request: %o"), error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the avatar file which is currently in "uploading" state to server
|
||||
* @param serverUniqueId
|
||||
*/
|
||||
async avatarUploadSucceeded(serverUniqueId: string) {
|
||||
if(!this.openedCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = await this.loadAvatarRequest(serverUniqueId, "uploading");
|
||||
if(request.status !== "success") {
|
||||
if(request.status !== "empty-result") {
|
||||
logError(LogCategory.GENERAL, tr("Failed to save uploaded avatar. Request failed to load: %o"), request);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.openedCache.put(OwnAvatarStorage.generateRequestUrl(serverUniqueId, "server"), request.result);
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to save uploaded avatar. Failed to store request: %o"), request);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.removeAvatar(serverUniqueId, "uploading");
|
||||
}
|
||||
}
|
||||
|
||||
let instance: OwnAvatarStorage;
|
||||
export function getOwnAvatarStorage(): OwnAvatarStorage {
|
||||
return instance;
|
||||
}
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
priority: 10,
|
||||
name: "own avatar storage",
|
||||
function: async () => {
|
||||
instance = new OwnAvatarStorage();
|
||||
await instance.initialize();
|
||||
}
|
||||
});
|
|
@ -17,21 +17,43 @@ export const downloadTextAsFile = (text: string, name: string) => {
|
|||
}, 0);
|
||||
};
|
||||
|
||||
export const requestFileAsText = async (): Promise<string> => {
|
||||
export const requestFile = async (options: {
|
||||
accept?: string,
|
||||
multiple?: boolean
|
||||
}): Promise<File[]> => {
|
||||
const element = document.createElement("input");
|
||||
element.style.display = "none";
|
||||
element.type = "file";
|
||||
|
||||
if(typeof options.accept === "string") {
|
||||
element.accept = options.accept;
|
||||
}
|
||||
|
||||
if(typeof options.multiple === "string") {
|
||||
element.multiple = options.multiple;
|
||||
}
|
||||
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
await new Promise(resolve => {
|
||||
element.onchange = resolve;
|
||||
});
|
||||
|
||||
if (element.files.length !== 1)
|
||||
return undefined;
|
||||
const file = element.files[0];
|
||||
element.remove();
|
||||
const result = [];
|
||||
for(let index = 0; index < element.files.length; index++) {
|
||||
result.push(element.files.item(index));
|
||||
}
|
||||
|
||||
return await file.text();
|
||||
element.remove();
|
||||
return result;
|
||||
}
|
||||
|
||||
export const requestFileAsText = async (): Promise<string> => {
|
||||
const files = await requestFile({ multiple: false });
|
||||
if(files.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/* FIXME: text() might not be available in Safari */
|
||||
return await files[0].text();
|
||||
};
|
|
@ -29,6 +29,7 @@ import {ActionResult} from "tc-services";
|
|||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
import {bookmarks} from "tc-shared/Bookmarks";
|
||||
import {getAudioBackend} from "tc-shared/audio/Player";
|
||||
|
||||
/* required import for init */
|
||||
import "svg-sprites/client-icons";
|
||||
|
@ -47,12 +48,12 @@ import "./ui/AppController";
|
|||
import "./ui/frames/menu-bar/MainMenu";
|
||||
import "./ui/modal/connect/Controller";
|
||||
import "./ui/modal/video-viewer/Controller";
|
||||
import "./ui/modal/avatar-upload/Controller";
|
||||
import "./ui/elements/ContextDivider";
|
||||
import "./ui/elements/Tab";
|
||||
import "./clientservice";
|
||||
import "./text/bbcode/InviteController";
|
||||
import "./text/bbcode/YoutubeController";
|
||||
import {getAudioBackend} from "tc-shared/audio/Player";
|
||||
|
||||
assertMainApplication();
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import {ClientGroupInfo, ClientInfoEvents,} from "tc-shared/ui/frames/side/Clien
|
|||
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo";
|
||||
import {spawnAvatarUpload} from "tc-shared/ui/modal/avatar-upload/Controller";
|
||||
|
||||
export class ClientInfoController {
|
||||
private readonly uiEvents: Registry<ClientInfoEvents>;
|
||||
|
@ -27,7 +28,13 @@ export class ClientInfoController {
|
|||
this.uiEvents.on("query_version", () => this.sendVersion());
|
||||
this.uiEvents.on("query_forum", () => this.sendForum());
|
||||
|
||||
this.uiEvents.on("action_edit_avatar", () => this.connection?.update_avatar());
|
||||
this.uiEvents.on("action_edit_avatar", () => {
|
||||
if(!this.connection?.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
spawnAvatarUpload(this.connection);
|
||||
});
|
||||
this.uiEvents.on("action_show_full_info", () => {
|
||||
const client = this.connection?.getSelectedClientInfo().getClient();
|
||||
if(client) {
|
||||
|
|
|
@ -0,0 +1,346 @@
|
|||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {Registry} from "tc-events";
|
||||
import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
||||
import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import PermissionType from "tc-shared/permission/PermissionType";
|
||||
import {getOwnAvatarStorage, LocalAvatarInfo} from "tc-shared/file/OwnAvatarStorage";
|
||||
import {LogCategory, logError, logInfo, logWarn} from "tc-shared/log";
|
||||
import {Mutex} from "tc-shared/Mutex";
|
||||
import {tr, traj} from "tc-shared/i18n/localize";
|
||||
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {FileTransferState, TransferProvider} from "tc-shared/file/Transfer";
|
||||
|
||||
class Controller {
|
||||
readonly connection: ConnectionHandler;
|
||||
readonly serverUniqueId: string;
|
||||
readonly events: Registry<ModalAvatarUploadEvents>;
|
||||
readonly variables: IpcUiVariableProvider<ModalAvatarUploadVariables>;
|
||||
|
||||
private registeredListener: (() => void)[];
|
||||
|
||||
private rendererUploading = false;
|
||||
private controllerLoading = false;
|
||||
private serverAvatarLoading = false;
|
||||
|
||||
private avatarInfo: LocalAvatarInfo | undefined;
|
||||
|
||||
/* TODO: Update the UI so the client can't upload multiple avatars at the same time (maybe even server wide?) */
|
||||
private serverAvatarMutex: Mutex<void>;
|
||||
private serverAvatarUrl: string;
|
||||
|
||||
constructor(connection: ConnectionHandler) {
|
||||
this.connection = connection;
|
||||
this.serverUniqueId = connection.getCurrentServerUniqueId();
|
||||
this.registeredListener = [];
|
||||
this.serverAvatarMutex = new Mutex<void>(void 0);
|
||||
|
||||
this.events = new Registry<ModalAvatarUploadEvents>();
|
||||
this.variables = new IpcUiVariableProvider<ModalAvatarUploadVariables>();
|
||||
|
||||
this.variables.setVariableProvider("maxAvatarSize", () => {
|
||||
const permission = this.connection.permissions.neededPermission(PermissionType.I_CLIENT_MAX_AVATAR_FILESIZE);
|
||||
return permission.valueOr(-1);
|
||||
});
|
||||
|
||||
this.variables.setVariableProvider("currentAvatar", () => {
|
||||
if(this.rendererUploading || this.controllerLoading || this.serverAvatarLoading) {
|
||||
return { status: "loading" };
|
||||
}
|
||||
|
||||
if(this.avatarInfo) {
|
||||
const maxSize = this.variables.getVariableSync("maxAvatarSize");
|
||||
return {
|
||||
status: maxSize >= 0 && maxSize < this.avatarInfo.fileSize ? "exceeds-max-size" : "available",
|
||||
|
||||
fileHashMD5: this.avatarInfo.fileHashMD5,
|
||||
fileName: this.avatarInfo.fileName,
|
||||
fileSize: this.avatarInfo.fileSize,
|
||||
|
||||
resourceUrl: this.avatarInfo.resourceUrl,
|
||||
|
||||
serverHasAvatar: this.serverAvatarUrl !== undefined
|
||||
};
|
||||
} else if(this.serverAvatarUrl) {
|
||||
return { status: "server", resourceUrl: this.serverAvatarUrl };
|
||||
} else {
|
||||
return { status: "unset" };
|
||||
}
|
||||
});
|
||||
|
||||
this.registeredListener.push(this.connection.permissions.register_needed_permission(PermissionType.I_CLIENT_MAX_AVATAR_FILESIZE, () => {
|
||||
this.variables.sendVariable("maxAvatarSize");
|
||||
this.variables.sendVariable("currentAvatar");
|
||||
}));
|
||||
|
||||
this.events.on("action_file_cache_loading", () => {
|
||||
this.rendererUploading = true;
|
||||
this.variables.sendVariable("currentAvatar");
|
||||
});
|
||||
|
||||
this.events.on("action_file_cache_loading_finished", event => {
|
||||
this.rendererUploading = false;
|
||||
if(!event.success) {
|
||||
/* Failed to update local avatar. Send the last one. */
|
||||
this.variables.sendVariable("currentAvatar");
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadLocalAvatar();
|
||||
});
|
||||
|
||||
this.loadServerAvatar();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.registeredListener.forEach(callback => callback());
|
||||
this.registeredListener = [];
|
||||
|
||||
this.events.destroy();
|
||||
this.variables.destroy();
|
||||
|
||||
this.setAvatarInfo(undefined);
|
||||
|
||||
this.serverAvatarMutex.execute(async () => {
|
||||
/* Cleanup the cache so the next upload will be fresh */
|
||||
await getOwnAvatarStorage().removeAvatar(this.serverUniqueId, "uploading");
|
||||
}).then(undefined);
|
||||
}
|
||||
|
||||
private loadLocalAvatar() {
|
||||
if(this.controllerLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadLocalAvatar0().catch(error => {
|
||||
logError(LogCategory.GENERAL, tr("Failed to load local cached avatar: %o"), error);
|
||||
this.events.fire("notify_avatar_load_error", { error: tr("Failed to load local cached avatar") });
|
||||
}).then(() => {
|
||||
this.controllerLoading = false;
|
||||
this.variables.sendVariable("currentAvatar");
|
||||
});
|
||||
}
|
||||
|
||||
private async loadLocalAvatar0() {
|
||||
const result = await getOwnAvatarStorage().loadAvatar(this.serverUniqueId, "uploading", true);
|
||||
let info: LocalAvatarInfo;
|
||||
switch (result.status) {
|
||||
case "success":
|
||||
info = result.result;
|
||||
break;
|
||||
|
||||
case "error":
|
||||
this.events.fire("notify_avatar_load_error", {error: result.reason});
|
||||
return;
|
||||
|
||||
case "cache-unavailable":
|
||||
this.events.fire("notify_avatar_load_error", {error: tr("Avatar cache unavailable")});
|
||||
return;
|
||||
|
||||
case "empty-result":
|
||||
this.setAvatarInfo(undefined);
|
||||
return;
|
||||
|
||||
default:
|
||||
throw tr("invalid state");
|
||||
}
|
||||
|
||||
this.setAvatarInfo(info);
|
||||
}
|
||||
|
||||
private loadServerAvatar() {
|
||||
if(this.serverAvatarLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.serverAvatarUrl = undefined;
|
||||
this.loadServerAvatar0().catch(error => {
|
||||
logError(LogCategory.GENERAL, tr("Failed to load server avatar: %o"), error);
|
||||
}).then(() => {
|
||||
this.serverAvatarLoading = false;
|
||||
this.variables.sendVariable("currentAvatar");
|
||||
})
|
||||
}
|
||||
|
||||
private async loadServerAvatar0() {
|
||||
const ownClientAvatar = this.connection.fileManager.avatars.resolveAvatar(this.connection.getClient().avatarId());
|
||||
await ownClientAvatar.awaitLoaded(5000);
|
||||
if(ownClientAvatar.getState() !== "loaded") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.serverAvatarUrl = ownClientAvatar.getTypedStateData("loaded").url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: This will not trigger the "currentAvatar" variable resend!
|
||||
* @param newInfo
|
||||
* @private
|
||||
*/
|
||||
private setAvatarInfo(newInfo: LocalAvatarInfo) {
|
||||
if(this.avatarInfo?.resourceUrl) {
|
||||
URL.revokeObjectURL(this.avatarInfo.resourceUrl);
|
||||
}
|
||||
|
||||
this.avatarInfo = newInfo;
|
||||
}
|
||||
|
||||
resetAvatar() {
|
||||
this.serverAvatarMutex.execute(async () => {
|
||||
this.setAvatarInfo(undefined);
|
||||
await getOwnAvatarStorage().removeAvatar(this.serverUniqueId, "uploading");
|
||||
|
||||
const serverConnection = this.connection.serverConnection;
|
||||
if(!serverConnection.connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await serverConnection.send_command('ftdeletefile', {
|
||||
name: "/avatar_", /* delete own avatar */
|
||||
path: "",
|
||||
cid: 0
|
||||
});
|
||||
|
||||
createInfoModal(tr("Avatar deleted"), tr("Avatar successfully deleted")).open();
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to reset avatar flag: %o"), error);
|
||||
|
||||
let message;
|
||||
if(error instanceof CommandResult) {
|
||||
message = formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.formattedMessage());
|
||||
}
|
||||
|
||||
if(!message) {
|
||||
message = formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details"));
|
||||
}
|
||||
|
||||
createErrorModal(tr("Failed to delete avatar"), message).open();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadServerAvatar();
|
||||
});
|
||||
}
|
||||
|
||||
uploadAvatar() {
|
||||
/* copy the avatar info */
|
||||
const avatarInfo = this.avatarInfo;
|
||||
this.serverAvatarMutex.execute(async() => {
|
||||
const serverConnection = this.connection.serverConnection;
|
||||
if(!serverConnection.connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!avatarInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logInfo(LogCategory.CLIENT, tr("Uploading new avatar"));
|
||||
const loadResult = await getOwnAvatarStorage().loadAvatarImage(this.serverUniqueId, "uploading");
|
||||
if (loadResult.status !== "success") {
|
||||
logError(LogCategory.GENERAL, tr("Failed to load cached avatar image: %o"), loadResult);
|
||||
throw tr("failed to load avatar image");
|
||||
}
|
||||
|
||||
const transfer = this.connection.fileManager.initializeFileUpload({
|
||||
name: "/avatar",
|
||||
path: "",
|
||||
|
||||
channel: 0,
|
||||
channelPassword: undefined,
|
||||
|
||||
source: async () => await TransferProvider.provider().createBufferSource(loadResult.result)
|
||||
});
|
||||
|
||||
await transfer.awaitFinished();
|
||||
|
||||
if (transfer.transferState() !== FileTransferState.FINISHED) {
|
||||
if (transfer.transferState() === FileTransferState.ERRORED) {
|
||||
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to upload clients avatar: %o"), transfer.currentError());
|
||||
createErrorModal(tr("Failed to upload avatar"), traj("Failed to upload avatar:{:br:}{0}", transfer.currentErrorMessage())).open();
|
||||
return;
|
||||
} else if (transfer.transferState() === FileTransferState.CANCELED) {
|
||||
createErrorModal(tr("Failed to upload avatar"), tr("Your avatar upload has been canceled.")).open();
|
||||
return;
|
||||
} else {
|
||||
createErrorModal(tr("Failed to upload avatar"), tr("Avatar upload finished with an unknown finished state.")).open();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error);
|
||||
createErrorModal(tr("Failed to upload avatar"), tr("Avatar upload failed. Lookup the console for more details.")).open();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.serverConnection.send_command('clientupdate', {
|
||||
client_flag_avatar: avatarInfo.fileHashMD5
|
||||
});
|
||||
} catch(error) {
|
||||
logError(LogCategory.GENERAL, tr("Failed to update avatar flag: %o"), error);
|
||||
|
||||
let message;
|
||||
if(error instanceof CommandResult) {
|
||||
message = formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.formattedMessage());
|
||||
}
|
||||
|
||||
if(!message) {
|
||||
message = formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details"));
|
||||
}
|
||||
|
||||
createErrorModal(tr("Failed to set avatar"), message).open();
|
||||
return;
|
||||
}
|
||||
|
||||
createInfoModal(tr("Avatar successfully uploaded"), tr("Your avatar has been uploaded successfully!")).open();
|
||||
this.loadServerAvatar();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnAvatarUpload(connection: ConnectionHandler) {
|
||||
const controller = new Controller(connection);
|
||||
|
||||
const modal = spawnModal("modal-avatar-upload", [
|
||||
controller.events.generateIpcDescription(),
|
||||
controller.variables.generateConsumerDescription(),
|
||||
connection.getCurrentServerUniqueId()
|
||||
], {
|
||||
popoutable: true
|
||||
});
|
||||
|
||||
controller.events.on("action_avatar_upload", event => {
|
||||
controller.uploadAvatar();
|
||||
if(event.closeWindow) {
|
||||
modal.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
controller.events.on("action_avatar_delete", event => {
|
||||
controller.resetAvatar();
|
||||
if(event.closeWindow) {
|
||||
modal.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
modal.getEvents().on("destroy", () => controller.destroy());
|
||||
modal.getEvents().on("destroy", connection.events().on("notify_connection_state_changed", event => {
|
||||
if(event.newState !== ConnectionState.CONNECTED) {
|
||||
modal.destroy();
|
||||
}
|
||||
}));
|
||||
|
||||
modal.show().then(undefined);
|
||||
|
||||
/* Trying to prompt the user */
|
||||
controller.events.fire("action_open_select");
|
||||
}
|
||||
|
||||
(window as any).test = () => spawnAvatarUpload(server_connections.getActiveConnectionHandler());
|
||||
setTimeout(() => (window as any).test(), 1500);
|
|
@ -0,0 +1,33 @@
|
|||
export type CurrentAvatarState = {
|
||||
status: "unset" | "loading"
|
||||
} | {
|
||||
status: "available" | "exceeds-max-size",
|
||||
|
||||
fileName: string,
|
||||
fileSize: number,
|
||||
fileHashMD5: string,
|
||||
|
||||
resourceUrl: string | undefined,
|
||||
|
||||
serverHasAvatar: boolean
|
||||
} | {
|
||||
status: "server",
|
||||
resourceUrl: string
|
||||
};
|
||||
|
||||
export interface ModalAvatarUploadVariables {
|
||||
readonly maxAvatarSize: number,
|
||||
readonly currentAvatar: CurrentAvatarState
|
||||
}
|
||||
|
||||
export interface ModalAvatarUploadEvents {
|
||||
action_open_select: {},
|
||||
|
||||
action_file_cache_loading: {},
|
||||
action_file_cache_loading_finished: { success: boolean },
|
||||
|
||||
action_avatar_upload: { closeWindow: boolean },
|
||||
action_avatar_delete: { closeWindow: boolean }
|
||||
|
||||
notify_avatar_load_error: { error: string }
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 1em;
|
||||
width: 30em;
|
||||
|
||||
&.windowed {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.containerSelect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.containerFile {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.button {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.name {
|
||||
align-self: center;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
}
|
||||
|
||||
.containerLimit {
|
||||
margin-top: .1em;
|
||||
margin-left: .25em;
|
||||
|
||||
font-size: .9em;
|
||||
color: #666;
|
||||
|
||||
&.error {
|
||||
color: #a32929;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containerPreview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
margin-top: 1em;
|
||||
|
||||
.title {
|
||||
color: #557edc;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.previews {
|
||||
margin-top: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
width: 10em;
|
||||
|
||||
.imageContainer {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: 50%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
|
||||
//background: #353535;
|
||||
box-shadow: inset 0 0 5px #00000040;
|
||||
|
||||
.image {
|
||||
align-self: center;
|
||||
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
&.heightBound {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.widthBound {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.sizeChatInfo {
|
||||
height: 10em;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
&.sizeChat {
|
||||
height: 2.5em;
|
||||
width: 2.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
padding-top: 1em;
|
||||
margin-top: auto;
|
||||
}
|
|
@ -0,0 +1,336 @@
|
|||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import React, {useContext} from "react";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||
import {
|
||||
CurrentAvatarState,
|
||||
ModalAvatarUploadEvents,
|
||||
ModalAvatarUploadVariables
|
||||
} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
||||
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {requestFile} from "tc-shared/file/Utils";
|
||||
import {network} from "tc-shared/ui/frames/chat";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {getOwnAvatarStorage} from "tc-shared/file/OwnAvatarStorage";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
import {joinClassList, useDependentState} from "tc-shared/ui/react-elements/Helper";
|
||||
|
||||
import kDefaultAvatarUrl from "../../../../img/style/avatar.png";
|
||||
import byteSizeToString = network.byteSizeToString;
|
||||
|
||||
const ServerUniqueIdContext = React.createContext<string>(undefined);
|
||||
const EventContext = React.createContext<Registry<ModalAvatarUploadEvents>>(undefined);
|
||||
const VariablesContext = React.createContext<UiVariableConsumer<ModalAvatarUploadVariables>>(undefined);
|
||||
const CurrentAvatarContext = React.createContext<CurrentAvatarState>(undefined);
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
const AvatarFileName = React.memo(() => {
|
||||
const currentAvatar = useContext(CurrentAvatarContext);
|
||||
switch (currentAvatar.status) {
|
||||
case "loading":
|
||||
return (
|
||||
<div className={cssStyle.name} key={"loading"}>
|
||||
<Translatable>Loading avatar</Translatable>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "server":
|
||||
case "unset":
|
||||
return (
|
||||
<div className={cssStyle.name} key={"unset"}>
|
||||
<Translatable>No avatar selected</Translatable>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "available":
|
||||
case "exceeds-max-size":
|
||||
return (
|
||||
<div className={cssStyle.name} key={"available"}>
|
||||
{currentAvatar.fileName}
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
throw "invalid avatar state";
|
||||
}
|
||||
});
|
||||
|
||||
const SizeLimitRenderer = React.memo((props: { byteSize: number }) => {
|
||||
if(props.byteSize === -1) {
|
||||
return <Translatable key={"unlimited"}>unlimited</Translatable>;
|
||||
} else {
|
||||
return <React.Fragment key={"limited"}>{byteSizeToString(props.byteSize)}</React.Fragment>;
|
||||
}
|
||||
});
|
||||
|
||||
const MaxAvatarSize = React.memo(() => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const currentAvatar = useContext(CurrentAvatarContext);
|
||||
const maxSize = variables.useReadOnly("maxAvatarSize", undefined);
|
||||
|
||||
if(currentAvatar.status === "loading" || maxSize.status === "loading") {
|
||||
return (
|
||||
<div className={cssStyle.containerLimit} key={"loading"}>
|
||||
<Translatable>Maximal avatar size:</Translatable> <Translatable>loading</Translatable> <LoadingDots />
|
||||
</div>
|
||||
)
|
||||
} else if(currentAvatar.status === "unset" || currentAvatar.status === "server") {
|
||||
return (
|
||||
<div className={cssStyle.containerLimit} key={"unset"}>
|
||||
<VariadicTranslatable text={"Maximal avatar size: {}"}>
|
||||
<SizeLimitRenderer byteSize={maxSize.value} />
|
||||
</VariadicTranslatable>
|
||||
</div>
|
||||
);
|
||||
} else if(currentAvatar.status === "available") {
|
||||
return (
|
||||
<div className={cssStyle.containerLimit} key={"size-ok"}>
|
||||
<VariadicTranslatable text={"Avatar size: {} / {}"}>
|
||||
{byteSizeToString(currentAvatar.fileSize)}
|
||||
<SizeLimitRenderer byteSize={maxSize.value} />
|
||||
</VariadicTranslatable>
|
||||
</div>
|
||||
);
|
||||
} else if(currentAvatar.status === "exceeds-max-size") {
|
||||
return (
|
||||
<div className={cssStyle.containerLimit + " " + cssStyle.error} key={"unset"}>
|
||||
<VariadicTranslatable text={"Avatar {} exceeds allowed size of {}"}>
|
||||
{byteSizeToString(currentAvatar.fileSize)}
|
||||
{maxSize.value === -1 ? <Translatable key={"unlimited"}>unlimited</Translatable> : byteSizeToString(maxSize.value)}
|
||||
</VariadicTranslatable>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
throw "invalid avatar state";
|
||||
}
|
||||
})
|
||||
|
||||
const AvatarSelect = React.memo(() => {
|
||||
const currentAvatar = useContext(CurrentAvatarContext);
|
||||
const events = useContext(EventContext);
|
||||
|
||||
return (
|
||||
<div className={cssStyle.containerSelect}>
|
||||
<div className={cssStyle.containerFile}>
|
||||
<Button
|
||||
onClick={() => events.fire("action_open_select")}
|
||||
className={cssStyle.button}
|
||||
disabled={currentAvatar.status === "loading"}
|
||||
>
|
||||
<Translatable>Select avatar</Translatable>
|
||||
</Button>
|
||||
<AvatarFileName />
|
||||
</div>
|
||||
<MaxAvatarSize />
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const AvatarPreview = React.memo((props: { size: "client-info" | "chat" }) => {
|
||||
const currentAvatar = useContext(CurrentAvatarContext);
|
||||
|
||||
let sizeClass;
|
||||
let name;
|
||||
switch (props.size) {
|
||||
case "client-info":
|
||||
sizeClass = cssStyle.sizeChatInfo;
|
||||
name = <Translatable key={"client-info"}>Client info</Translatable>;
|
||||
break;
|
||||
|
||||
case "chat":
|
||||
sizeClass = cssStyle.sizeChat;
|
||||
name = <Translatable key={"chat-avatar"}>Chat Avatar</Translatable>;
|
||||
break;
|
||||
}
|
||||
|
||||
let imageUrl;
|
||||
if(currentAvatar.status === "available" || currentAvatar.status === "exceeds-max-size") {
|
||||
imageUrl = currentAvatar.resourceUrl;
|
||||
} else if(currentAvatar.status === "server") {
|
||||
imageUrl = currentAvatar.resourceUrl;
|
||||
} else {
|
||||
imageUrl = kDefaultAvatarUrl;
|
||||
}
|
||||
|
||||
const [ boundClass, setBoundClass ] = useDependentState(() => undefined, [ imageUrl ]);
|
||||
|
||||
return (
|
||||
<div className={cssStyle.preview}>
|
||||
<div className={cssStyle.imageContainer + " " + sizeClass}>
|
||||
<img
|
||||
className={joinClassList(cssStyle.image, boundClass)}
|
||||
src={imageUrl}
|
||||
alt={""}
|
||||
onLoad={event => {
|
||||
const imageElement = event.currentTarget;
|
||||
if(imageElement.naturalHeight > imageElement.naturalWidth) {
|
||||
setBoundClass(cssStyle.heightBound);
|
||||
} else {
|
||||
setBoundClass(cssStyle.widthBound);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={cssStyle.name}>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const PreviewTitle = React.memo(() => {
|
||||
const currentAvatar = useContext(CurrentAvatarContext);
|
||||
if(currentAvatar.status === "server") {
|
||||
return <Translatable key={"server"}>Current Avatar</Translatable>;
|
||||
} else {
|
||||
return <Translatable key={"general"}>Preview</Translatable>;
|
||||
}
|
||||
});
|
||||
|
||||
const AvatarPreviewContainer = React.memo(() => (
|
||||
<div className={cssStyle.containerPreview}>
|
||||
<div className={cssStyle.title}><PreviewTitle /></div>
|
||||
<div className={cssStyle.previews}>
|
||||
<AvatarPreview size={"client-info"} />
|
||||
<AvatarPreview size={"chat"} />
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
const Buttons = React.memo(() => {
|
||||
const events = useContext(EventContext);
|
||||
const currentAvatar = useContext(CurrentAvatarContext);
|
||||
|
||||
let enableReset, enableUpload;
|
||||
switch (currentAvatar.status) {
|
||||
case "exceeds-max-size":
|
||||
enableReset = currentAvatar.serverHasAvatar;
|
||||
enableUpload = false;
|
||||
break;
|
||||
|
||||
case "loading":
|
||||
case "unset":
|
||||
enableReset = false;
|
||||
enableUpload = false;
|
||||
break;
|
||||
|
||||
case "available":
|
||||
enableReset = true;
|
||||
enableUpload = true;
|
||||
break;
|
||||
|
||||
case "server":
|
||||
enableReset = true;
|
||||
enableUpload = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.buttons}>
|
||||
<Button
|
||||
color={"red"}
|
||||
onClick={event => events.fire("action_avatar_delete", { closeWindow: !event.shiftKey })}
|
||||
disabled={!enableReset}
|
||||
>
|
||||
<Translatable>Delete Avatar</Translatable>
|
||||
</Button>
|
||||
<Button
|
||||
color={"green"}
|
||||
onClick={event => events.fire("action_avatar_upload", { closeWindow: !event.shiftKey })}
|
||||
disabled={!enableUpload}
|
||||
>
|
||||
<Translatable>Update avatar</Translatable>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const CurrentAvatarProvider = React.memo((props: { children }) => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const avatar = variables.useReadOnly("currentAvatar", undefined, { status: "loading" });
|
||||
return (
|
||||
<CurrentAvatarContext.Provider value={avatar}>
|
||||
{props.children}
|
||||
</CurrentAvatarContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
class ModalAvatarUpload extends AbstractModal {
|
||||
private readonly serverUniqueId: string;
|
||||
private readonly events: Registry<ModalAvatarUploadEvents>;
|
||||
private readonly variables: UiVariableConsumer<ModalAvatarUploadVariables>;
|
||||
|
||||
constructor(events: IpcRegistryDescription<ModalAvatarUploadEvents>, variables: IpcVariableDescriptor<ModalAvatarUploadVariables>, serverUniqueId: string) {
|
||||
super();
|
||||
|
||||
this.serverUniqueId = serverUniqueId;
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
this.variables = createIpcUiVariableConsumer(variables);
|
||||
|
||||
this.events.on("notify_avatar_load_error", event => {
|
||||
createErrorModal(tr("Failed to load avatar"), event.error).open();
|
||||
});
|
||||
|
||||
this.events.on("action_open_select", async () => {
|
||||
this.events.fire("action_file_cache_loading");
|
||||
const files = await requestFile({
|
||||
multiple: false,
|
||||
accept: ".svg, .png, .jpg, .jpeg, gif"
|
||||
});
|
||||
|
||||
if(files.length !== 1) {
|
||||
this.events.fire("action_file_cache_loading_finished", { success: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await getOwnAvatarStorage().updateAvatar(serverUniqueId, "uploading", files[0]);
|
||||
let succeeded;
|
||||
if(result.status === "success") {
|
||||
succeeded = true;
|
||||
} else if(result.status === "error") {
|
||||
createErrorModal(tr("Failed to load avatar"), tra("Failed to load avatar: {}", result.reason)).open();
|
||||
succeeded = false;
|
||||
} else if(result.status === "cache-unavailable") {
|
||||
createErrorModal(tr("Failed to load avatar"), tra("Failed to load avatar:{:br:}Own avatar cach unavailable.")).open();
|
||||
succeeded = false;
|
||||
} else {
|
||||
succeeded = false;
|
||||
}
|
||||
|
||||
this.events.fire("action_file_cache_loading_finished", { success: succeeded });
|
||||
});
|
||||
}
|
||||
|
||||
protected onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
this.events.destroy();
|
||||
this.variables.destroy();
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EventContext.Provider value={this.events}>
|
||||
<VariablesContext.Provider value={this.variables}>
|
||||
<ServerUniqueIdContext.Provider value={this.serverUniqueId}>
|
||||
<CurrentAvatarProvider>
|
||||
<div className={cssStyle.container + " " + (this.properties.windowed ? cssStyle.windowed : "")}>
|
||||
<AvatarSelect />
|
||||
<AvatarPreviewContainer />
|
||||
<Buttons />
|
||||
</div>
|
||||
</CurrentAvatarProvider>
|
||||
</ServerUniqueIdContext.Provider>
|
||||
</VariablesContext.Provider>
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle(): string | React.ReactElement {
|
||||
return <Translatable>Avatar upload</Translatable>;
|
||||
}
|
||||
}
|
||||
export default ModalAvatarUpload;
|
|
@ -19,6 +19,7 @@ import {VideoViewerEvents} from "tc-shared/ui/modal/video-viewer/Definitions";
|
|||
import {PermissionModalEvents} from "tc-shared/ui/modal/permission/ModalDefinitions";
|
||||
import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefinitions";
|
||||
import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer";
|
||||
import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
||||
|
||||
export type ModalType = "error" | "warning" | "info" | "none";
|
||||
export type ModalRenderType = "page" | "dialog";
|
||||
|
@ -204,5 +205,10 @@ export interface ModalConstructorArguments {
|
|||
/* serverInfo */ PermissionEditorServerInfo,
|
||||
/* modalEvents */ IpcRegistryDescription<PermissionModalEvents>,
|
||||
/* editorEvents */ IpcRegistryDescription<PermissionEditorEvents>
|
||||
],
|
||||
"modal-avatar-upload": [
|
||||
/* events */ IpcRegistryDescription<ModalAvatarUploadEvents>,
|
||||
/* variables */ IpcVariableDescriptor<ModalAvatarUploadVariables>,
|
||||
/* serverUniqueId */ string
|
||||
]
|
||||
}
|
|
@ -103,4 +103,10 @@ registerModal({
|
|||
popoutSupported: true
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "modal-avatar-upload",
|
||||
classLoader: async () => await import("tc-shared/ui/modal/avatar-upload/Renderer"),
|
||||
popoutSupported: true
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -330,6 +330,11 @@ export const config = async (env: any, target: "web" | "client"): Promise<Config
|
|||
"tc-events": path.resolve(__dirname, "vendor/TeaEventBus/src/index.ts"),
|
||||
"tc-services": path.resolve(__dirname, "vendor/TeaClientServices/src/index.ts"),
|
||||
},
|
||||
fallback: {
|
||||
stream: "stream-browserify",
|
||||
crypto: "crypto-browserify",
|
||||
buffer: "buffer"
|
||||
}
|
||||
},
|
||||
externals: [
|
||||
{"tc-loader": "window loader"}
|
||||
|
|
Loading…
Reference in New Issue