diff --git a/ChangeLog.md b/ChangeLog.md index 9326f41c..80948153 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,8 @@ # Changelog: +* **03.05.20** + - Splitup the file transfer & management part + - Added the ability to register a custom file transfer provider (required for the native client) + * **25.04.20** - Fixed missing channel tree update on talk power change diff --git a/package-lock.json b/package-lock.json index 61eb4d30..c0ea6589 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,22 @@ "protobufjs": "^6.8.6" } }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -691,6 +707,48 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "ansi-colors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", @@ -700,6 +758,12 @@ "ansi-wrap": "^0.1.0" } }, + "ansi-escapes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-2.0.0.tgz", + "integrity": "sha1-W65SvkJIeN2Xg+iRDj/Cki6DyBs=", + "dev": true + }, "ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -1232,6 +1296,89 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dev": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1454,6 +1601,12 @@ } } }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, "camel-case": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", @@ -1488,6 +1641,12 @@ } } }, + "capture-stack-trace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", + "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1554,6 +1713,12 @@ "webpack-core": "^0.6.9" } }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -1633,6 +1798,12 @@ "del": "^4.1.1" } }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", @@ -1796,6 +1967,37 @@ "typedarray": "^0.0.6" } }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "dev": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -1908,6 +2110,15 @@ "elliptic": "^6.0.0" } }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -1964,6 +2175,12 @@ "randomfill": "^1.0.3" } }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, "css-loader": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz", @@ -2142,12 +2359,28 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + } + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, "default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -2307,6 +2540,32 @@ "randombytes": "^2.0.0" } }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + }, + "dependencies": { + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -2373,12 +2632,27 @@ "tslib": "^1.10.0" } }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -2601,6 +2875,81 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "eslint-formatter-pretty": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-1.3.0.tgz", + "integrity": "sha512-5DY64Y1rYCm7cfFDHEGUn54bvCnK+wSUVF07N8oXeqUJFSd+gnYOTXbzelQ1HurESluY6gnEQPmXOIkB4Wa+gA==", + "dev": true, + "requires": { + "ansi-escapes": "^2.0.0", + "chalk": "^2.1.0", + "log-symbols": "^2.0.0", + "plur": "^2.1.2", + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -2852,6 +3201,332 @@ "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "dev": true }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4177,6 +4852,12 @@ } } }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, "glob-watcher": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", @@ -4541,6 +5222,15 @@ } } }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, "global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", @@ -4672,6 +5362,33 @@ "node-forge": "^0.9.0" } }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } + } + }, "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", @@ -5214,6 +5931,18 @@ "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", "dev": true }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -5297,6 +6026,12 @@ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true }, + "irregular-plurals": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz", + "integrity": "sha1-LKmwM2UREYVUEvFr5dd8YqRYp2Y=", + "dev": true + }, "is": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", @@ -5349,6 +6084,15 @@ "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", "dev": true }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -5443,12 +6187,39 @@ "html-tags": "^3.0.0" } }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + }, + "dependencies": { + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + } + } + }, "is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", "dev": true }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, "is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", @@ -5458,6 +6229,12 @@ "kind-of": "^3.0.2" } }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, "is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -5517,6 +6294,12 @@ "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", "dev": true }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, "is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", @@ -5535,6 +6318,12 @@ "is-unc-path": "^1.0.0" } }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -5789,6 +6578,15 @@ "es6-weak-map": "^2.0.1" } }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "^4.0.0" + } + }, "lazystream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", @@ -5904,6 +6702,46 @@ "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", "dev": true }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -5937,6 +6775,12 @@ "tslib": "^1.10.0" } }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -6414,6 +7258,12 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", + "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -6530,6 +7380,24 @@ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + }, + "dependencies": { + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + } + } + }, "minipass": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", @@ -7235,6 +8103,18 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -7491,6 +8371,15 @@ } } }, + "plur": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", + "integrity": "sha1-dIJFLBoPUI4+NE6uwxLJHCncZVo=", + "dev": true, + "requires": { + "irregular-plurals": "^1.0.0" + } + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -7790,6 +8679,12 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, "randomatic": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", @@ -7862,6 +8757,26 @@ "schema-utils": "^2.5.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, "react": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", @@ -8249,6 +9164,25 @@ "safe-regex": "^1.1.0" } }, + "registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "dev": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "^1.0.1" + } + }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -8799,6 +9733,15 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "^5.0.3" + } + }, "semver-greatest-satisfied-range": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", @@ -8954,6 +9897,12 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -9389,6 +10338,12 @@ "get-stdin": "^4.0.1" } }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", @@ -9459,6 +10414,49 @@ } } }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "^0.7.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } + } + }, "terser": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", @@ -9513,6 +10511,12 @@ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "dev": true }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, "timers-browserify": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", @@ -9719,6 +10723,301 @@ } } }, + "tsd": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.11.0.tgz", + "integrity": "sha512-klKMNC0KRzUIaLJG8XqkvH/9rKwYX74xpqJBN8spWjYUDojAesd6AfDCT5dray+yhLfTGkem7O3nU6i4KwzNDw==", + "dev": true, + "requires": { + "eslint-formatter-pretty": "^1.3.0", + "globby": "^9.1.0", + "meow": "^5.0.0", + "path-exists": "^3.0.0", + "read-pkg-up": "^4.0.0", + "update-notifier": "^2.5.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "globby": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", + "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^1.0.2", + "dir-glob": "^2.2.2", + "fast-glob": "^2.2.6", + "glob": "^7.1.3", + "ignore": "^4.0.3", + "pify": "^4.0.1", + "slash": "^2.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", + "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0", + "yargs-parser": "^10.0.0" + }, + "dependencies": { + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + } + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + } + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", @@ -9859,6 +11158,15 @@ "through2-filter": "^3.0.0" } }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -9917,12 +11225,67 @@ } } }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, "upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "upper-case": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", @@ -9962,6 +11325,15 @@ } } }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -11369,6 +12741,48 @@ "string-width": "^1.0.2 || 2" } }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "dev": true, + "requires": { + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", @@ -11403,6 +12817,17 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, "ws": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", @@ -11412,6 +12837,12 @@ "async-limiter": "~1.0.0" } }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 5cb9224f..618bff10 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "webpack": "^4.42.1", "webpack-bundle-analyzer": "^3.6.1", "webpack-cli": "^3.3.11", - "worker-plugin": "^4.0.2" + "worker-plugin": "^4.0.2", + "tsd": "latest" }, "repository": { "type": "git", diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 7bcc2f2a..db8856e0 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -19,7 +19,7 @@ import {InputStartResult, InputState} from "tc-shared/voice/RecorderBase"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; import {guid} from "tc-shared/crypto/uid"; import * as bipc from "./BrowserIPC"; -import {FileManager, spawn_upload_transfer, UploadKey} from "tc-shared/FileManager"; +import {FileManager, transfer_provider, UploadKey} from "tc-shared/file/FileManager"; import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; import {Frame} from "tc-shared/ui/frames/chat_frame"; import {Hostbanner} from "tc-shared/ui/frames/hostbanner"; @@ -900,7 +900,7 @@ export class ConnectionHandler { } try { - await spawn_upload_transfer(key).put_data(data); + await transfer_provider().spawn_upload_transfer(key).put_data(data); } catch(error) { log.error(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error); diff --git a/shared/js/FileManager.tsx b/shared/js/FileManager.tsx deleted file mode 100644 index 6dada06d..00000000 --- a/shared/js/FileManager.tsx +++ /dev/null @@ -1,1226 +0,0 @@ -import * as log from "tc-shared/log"; -import {LogCategory} from "tc-shared/log"; -import * as hex from "tc-shared/crypto/hex"; -import {ChannelEntry} from "tc-shared/ui/channel"; -import {ConnectionHandler} from "tc-shared/ConnectionHandler"; -import {ServerCommand} from "tc-shared/connection/ConnectionBase"; -import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; -import {ClientEntry} from "tc-shared/ui/client"; -import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler"; -import {Registry} from "tc-shared/events"; -import {format_time} from "tc-shared/ui/frames/chat"; - -export class FileEntry { - name: string; - datetime: number; - type: number; - size: number; -} - -export class FileListRequest { - path: string; - entries: FileEntry[]; - - callback: (entries: FileEntry[]) => void; -} - -export interface TransferKey { - client_transfer_id: number; - server_transfer_id: number; - - key: string; - - file_path: string; - file_name: string; - - peer: { - hosts: string[], - port: number; - }; - - total_size: number; -} - -export interface UploadOptions { - name: string; - path: string; - - channel?: ChannelEntry; - channel_password?: string; - - size: number; - overwrite: boolean; -} - -export interface DownloadTransfer { - get_key() : DownloadKey; - - request_file() : Promise; -} - -export interface UploadTransfer { - get_key(): UploadKey; - - put_data(data: BlobPart | File) : Promise; -} - -export type DownloadKey = TransferKey; -export type UploadKey = TransferKey; - -export function spawn_download_transfer(key: DownloadKey) : DownloadTransfer { - return new RequestFileDownload(key); -} -export function spawn_upload_transfer(key: UploadKey) : UploadTransfer { - return new RequestFileUpload(key); -} - -export class RequestFileDownload implements DownloadTransfer { - readonly transfer_key: DownloadKey; - - constructor(key: DownloadKey) { - this.transfer_key = key; - } - - async request_file() : Promise { - return await this.try_fetch("https://" + this.transfer_key.peer.hosts[0] + ":" + this.transfer_key.peer.port); - } - - private async try_fetch(url: string) : Promise { - const response = await fetch(url, { - method: 'GET', - cache: "no-cache", - mode: 'cors', - headers: { - 'transfer-key': this.transfer_key.key, - 'download-name': this.transfer_key.file_name, - 'Access-Control-Allow-Headers': '*', - 'Access-Control-Expose-Headers': '*' - } - }); - if(!response.ok) { - debugger; - throw (response.type == 'opaque' || response.type == 'opaqueredirect' ? "invalid cross origin flag! May target isn't a TeaSpeak server?" : response.statusText || "response is not ok"); - } - return response; - } - - get_key(): DownloadKey { - return this.transfer_key; - } -} - -export class RequestFileUpload implements UploadTransfer { - readonly transfer_key: UploadKey; - constructor(key: DownloadKey) { - this.transfer_key = key; - } - - get_key(): UploadKey { - return this.transfer_key; - } - - async put_data(data: BlobPart | File) : Promise { - const form_data = new FormData(); - - if(data instanceof File) { - if(data.size != this.transfer_key.total_size) - throw "invalid size"; - - form_data.append("file", data); - } else if(typeof(data) === "string") { - if(data.length != this.transfer_key.total_size) - throw "invalid size"; - form_data.append("file", new Blob([data], { type: "application/octet-stream" })); - } else { - const buffer = data as BufferSource; - if(buffer.byteLength != this.transfer_key.total_size) - throw "invalid size"; - - form_data.append("file", new Blob([buffer], { type: "application/octet-stream" })); - } - - await this.try_put(form_data, "https://" + this.transfer_key.peer.hosts[0] + ":" + this.transfer_key.peer.port); - } - - private async try_put(data: FormData, url: string) : Promise { - const response = await fetch(url, { - method: 'POST', - cache: "no-cache", - mode: 'cors', - body: data, - headers: { - 'transfer-key': this.transfer_key.key, - 'Access-Control-Allow-Headers': '*', - 'Access-Control-Expose-Headers': '*' - } - }); - if(!response.ok) - throw (response.type == 'opaque' || response.type == 'opaqueredirect' ? "invalid cross origin flag! May target isn't a TeaSpeak server?" : response.statusText || "response is not ok"); - } -} - -export class FileManager extends AbstractCommandHandler { - handle: ConnectionHandler; - icons: IconManager; - avatars: AvatarManager; - - private listRequests: FileListRequest[] = []; - private pending_download_requests: DownloadKey[] = []; - private pending_upload_requests: UploadKey[] = []; - - private transfer_counter : number = 1; - - constructor(client: ConnectionHandler) { - super(client.serverConnection); - - this.handle = client; - this.icons = new IconManager(this); - this.avatars = new AvatarManager(this); - - this.connection.command_handler_boss().register_handler(this); - } - - destroy() { - if(this.connection) { - const hboss = this.connection.command_handler_boss(); - if(hboss) - hboss.unregister_handler(this); - } - - this.listRequests = undefined; - this.pending_download_requests = undefined; - this.pending_upload_requests = undefined; - - this.icons && this.icons.destroy(); - this.icons = undefined; - - this.avatars && this.avatars.destroy(); - this.avatars = undefined; - } - - handle_command(command: ServerCommand): boolean { - switch (command.command) { - case "notifyfilelist": - this.notifyFileList(command.arguments); - return true; - case "notifyfilelistfinished": - this.notifyFileListFinished(command.arguments); - return true; - case "notifystartdownload": - this.notifyStartDownload(command.arguments); - return true; - case "notifystartupload": - this.notifyStartUpload(command.arguments); - return true; - } - return false; - } - - - /******************************** File list ********************************/ - //TODO multiple requests (same path) - requestFileList(path: string, channel?: ChannelEntry, password?: string) : Promise { - const _this = this; - return new Promise((accept, reject) => { - let req = new FileListRequest(); - req.path = path; - req.entries = []; - req.callback = accept; - _this.listRequests.push(req); - - _this.handle.serverConnection.send_command("ftgetfilelist", {"path": path, "cid": (channel ? channel.channelId : "0"), "cpw": (password ? password : "")}).then(() => {}).catch(reason => { - _this.listRequests.remove(req); - if(reason instanceof CommandResult) { - if(reason.id == 0x0501) { - accept([]); //Empty result - return; - } - } - reject(reason); - }); - }); - } - - private notifyFileList(json) { - let entry : FileListRequest = undefined; - - for(let e of this.listRequests) { - if(e.path == json[0]["path"]){ - entry = e; - break; - } - } - - if(!entry) { - log.error(LogCategory.CLIENT, tr("Invalid file list entry. Path: %s"), json[0]["path"]); - return; - } - for(let e of (json as Array)) { - e.datetime = parseInt(e.datetime + ""); - e.size = parseInt(e.size + ""); - e.type = parseInt(e.type + ""); - entry.entries.push(e); - } - } - - private notifyFileListFinished(json) { - let entry : FileListRequest = undefined; - - for(let e of this.listRequests) { - if(e.path == json[0]["path"]){ - entry = e; - this.listRequests.remove(e); - break; - } - } - - if(!entry) { - log.error(LogCategory.CLIENT, tr("Invalid file list entry finish. Path: "), json[0]["path"]); - return; - } - entry.callback(entry.entries); - } - - - /******************************** File download/upload ********************************/ - download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise { - const transfer_data: DownloadKey = { - file_name: file, - file_path: path, - client_transfer_id: this.transfer_counter++ - } as any; - - this.pending_download_requests.push(transfer_data); - return new Promise((resolve, reject) => { - transfer_data["_callback"] = resolve; - this.handle.serverConnection.send_command("ftinitdownload", { - "path": path, - "name": file, - "cid": (channel ? channel.channelId : "0"), - "cpw": (password ? password : ""), - "clientftfid": transfer_data.client_transfer_id, - "seekpos": 0, - "proto": 1 - }, {process_result: false}).catch(reason => { - this.pending_download_requests.remove(transfer_data); - reject(reason); - }) - }); - } - - upload_file(options: UploadOptions) : Promise { - const transfer_data: UploadKey = { - file_path: options.path, - file_name: options.name, - client_transfer_id: this.transfer_counter++, - total_size: options.size - } as any; - - this.pending_upload_requests.push(transfer_data); - return new Promise((resolve, reject) => { - transfer_data["_callback"] = resolve; - this.handle.serverConnection.send_command("ftinitupload", { - "path": options.path, - "name": options.name, - "cid": (options.channel ? options.channel.channelId : "0"), - "cpw": options.channel_password || "", - "clientftfid": transfer_data.client_transfer_id, - "size": options.size, - "overwrite": options.overwrite, - "resume": false, - "proto": 1 - }).catch(reason => { - this.pending_upload_requests.remove(transfer_data); - reject(reason); - }) - }); - } - - private notifyStartDownload(json) { - json = json[0]; - - let clientftfid = parseInt(json["clientftfid"]); - let transfer: DownloadKey; - for(let e of this.pending_download_requests) - if(e.client_transfer_id == clientftfid) { - transfer = e; - break; - } - - transfer.server_transfer_id = parseInt(json["serverftfid"]); - transfer.key = json["ftkey"]; - transfer.total_size = json["size"]; - - transfer.peer = { - hosts: (json["ip"] || "").split(","), - port: parseInt(json["port"]) - }; - - if(transfer.peer.hosts.length == 0) - transfer.peer.hosts.push("0.0.0.0"); - - if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0') - transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host; - - (transfer["_callback"] as (val: DownloadKey) => void)(transfer); - this.pending_download_requests.remove(transfer); - } - - private notifyStartUpload(json) { - json = json[0]; - - let transfer: UploadKey; - let clientftfid = parseInt(json["clientftfid"]); - for(let e of this.pending_upload_requests) - if(e.client_transfer_id == clientftfid) { - transfer = e; - break; - } - - transfer.server_transfer_id = parseInt(json["serverftfid"]); - transfer.key = json["ftkey"]; - - transfer.peer = { - hosts: (json["ip"] || "").split(","), - port: parseInt(json["port"]) - }; - - if(transfer.peer.hosts.length == 0) - transfer.peer.hosts.push("0.0.0.0"); - - if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0') - transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host; - - (transfer["_callback"] as (val: UploadKey) => void)(transfer); - this.pending_upload_requests.remove(transfer); - } - - /** File management **/ - async delete_file(props: { - name: string, - path?: string; - cid?: number; - cpw?: string; - }) : Promise { - if(!props.name) - throw "invalid name!"; - - try { - await this.handle.serverConnection.send_command("ftdeletefile", { - cid: props.cid || 0, - cpw: props.cpw, - path: props.path || "", - name: props.name - }) - } catch(error) { - throw error; - } - } -} - -export enum ImageType { - UNKNOWN, - BITMAP, - PNG, - GIF, - SVG, - JPEG -} - -export function media_image_type(type: ImageType, file?: boolean) { - switch (type) { - case ImageType.BITMAP: - return "bmp"; - case ImageType.GIF: - return "gif"; - case ImageType.SVG: - return file ? "svg" : "svg+xml"; - case ImageType.JPEG: - return "jpeg"; - case ImageType.UNKNOWN: - case ImageType.PNG: - default: - return "png"; - } -} - -export function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) { - const ab2str10 = () => { - const buf = new Uint8Array(encoded_data as ArrayBuffer); - if(buf.byteLength < 10) - return ""; - - let result = ""; - for(let index = 0; index < 10; index++) - result += String.fromCharCode(buf[index]); - return result; - }; - - const bin = typeof(encoded_data) === "string" ? ((typeof(base64_encoded) === "undefined" || base64_encoded) ? atob(encoded_data) : encoded_data) : ab2str10(); - if(bin.length < 10) return ImageType.UNKNOWN; - - if(bin[0] == String.fromCharCode(66) && bin[1] == String.fromCharCode(77)) { - return ImageType.BITMAP; - } else if(bin.substr(0, 8) == "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a") { - return ImageType.PNG; - } else if(bin.substr(0, 4) == "\x47\x49\x46\x38" && (bin[4] == '\x37' || bin[4] == '\x39') && bin[5] == '\x61') { - return ImageType.GIF; - } else if(bin[0] == '\x3c') { - return ImageType.SVG; - } else if(bin[0] == '\xFF' && bin[1] == '\xd8') { - return ImageType.JPEG; - } - - return ImageType.UNKNOWN; -} - -export class CacheManager { - readonly cache_name: string; - - private _cache_category: Cache; - - constructor(name: string) { - this.cache_name = name; - } - - setupped() : boolean { return !!this._cache_category; } - - async reset() { - if(!window.caches) - return; - - try { - await caches.delete(this.cache_name); - } catch(error) { - throw "Failed to delete cache: " + error; - } - try { - await this.setup(); - } catch(error) { - throw "Failed to reinitialize cache!"; - } - } - - async setup() { - if(!window.caches) - throw "Missing caches!"; - - this._cache_category = await caches.open(this.cache_name); - } - - async cleanup(max_age: number) { - /* FIXME: TODO */ - } - - async resolve_cached(key: string, max_age?: number) : Promise { - max_age = typeof(max_age) === "number" ? max_age : -1; - - const cached_response = await this._cache_category.match("https://_local_cache/cache_request_" + key); - if(!cached_response) - return undefined; - - /* FIXME: Max age */ - return cached_response; - } - - async put_cache(key: string, value: Response, type?: string, headers?: {[key: string]:string}) { - const new_headers = new Headers(); - for(const key of value.headers.keys()) - new_headers.set(key, value.headers.get(key)); - if(type) - new_headers.set("Content-type", type); - for(const key of Object.keys(headers || {})) - new_headers.set(key, headers[key]); - - await this._cache_category.put("https://_local_cache/cache_request_" + key, new Response(value.body, { - headers: new_headers - })); - } - - async delete(key: string) { - const flag = await this._cache_category.delete("https://_local_cache/cache_request_" + key, { - ignoreVary: true, - ignoreMethod: true, - ignoreSearch: true - }); - if(!flag) { - console.warn(tr("Failed to delete key %s from cache!"), flag); - } - } -} - -const icon_cache: CacheManager = new CacheManager("icons"); -export interface IconManagerEvents { - notify_icon_state_changed: { - icon_id: number, - server_unique_id: string, - - icon: LocalIcon - }, -} - -//TODO: Invalidate icon after certain time if loading has failed and try to redownload (only if an icon loader has been set!) -type IconLoader = (icon?: LocalIcon) => Promise; -export class LocalIcon { - readonly icon_id: number; - readonly server_unique_id: string; - readonly status_change_callbacks: ((icon?: LocalIcon) => void)[] = []; - - status: "loading" | "loaded" | "empty" | "error" | "destroyed"; - - loaded_url?: string; - error_message?: string; - - private callback_icon_loader: IconLoader; - - constructor(id: number, server: string, loader_or_response: Response | IconLoader | undefined) { - this.icon_id = id; - this.server_unique_id = server; - - if(id >= 0 && id <= 1000) { - /* Internal TeaSpeak icons. These must be handled differently! */ - this.status = "loaded"; - } else { - this.status = "loading"; - if(loader_or_response instanceof Response) { - this.set_image(loader_or_response).catch(error => { - log.error(LogCategory.GENERAL, tr("Icon set image method threw an unexpected error: %o"), error); - this.status = "error"; - this.error_message = "unexpected parse error"; - this.triggerStatusChange(); - }); - } else { - this.callback_icon_loader = loader_or_response; - this.load().catch(error => { - log.error(LogCategory.GENERAL, tr("Icon load method threw an unexpected error: %o"), error); - this.status = "error"; - this.error_message = "unexpected load error"; - this.triggerStatusChange(); - }).then(() => { - this.callback_icon_loader = undefined; /* release resources captured by possible closures */ - }); - } - } - } - - private triggerStatusChange() { - for(const lister of this.status_change_callbacks.slice(0)) - lister(this); - } - - /* called within the CachedIconManager */ - protected destroy() { - if(typeof this.loaded_url === "string" && URL.revokeObjectURL) - URL.revokeObjectURL(this.loaded_url); - - this.status = "destroyed"; - this.loaded_url = undefined; - this.error_message = undefined; - - this.triggerStatusChange(); - this.status_change_callbacks.splice(0, this.status_change_callbacks.length); - } - - private async load() { - if(!icon_cache.setupped()) - await icon_cache.setup(); - - let response = await icon_cache.resolve_cached("icon_" + this.server_unique_id + "_" + this.icon_id); //TODO age! - if(!response) { - if(typeof this.callback_icon_loader !== "function") { - this.status = "empty"; - this.triggerStatusChange(); - return; - } - - try { - response = await this.callback_icon_loader(this); - } catch (error) { - log.warn(LogCategory.GENERAL, tr("Failed to download icon %d: %o"), this.icon_id, error); - await this.set_error(typeof error === "string" ? error : tr("Failed to load icon")); - return; - } - try { - await this.set_image(response); - } catch (error) { - log.error(LogCategory.GENERAL, tr("Failed to update icon image for icon %d: %o"), this.icon_id, error); - await this.set_error(typeof error === "string" ? error : tr("Failed to update icon from downloaded file")); - return; - } - return; - } - - this.loaded_url = await response_to_url(response); - this.status = "loaded"; - this.triggerStatusChange(); - } - - async set_image(response: Response) { - if(this.icon_id >= 0 && this.icon_id <= 1000) throw "Could not set image for internal icon"; - - const type = image_type(response.headers.get('X-media-bytes')); - if(type === ImageType.UNKNOWN) throw "unknown image type"; - - const media = media_image_type(type); - await icon_cache.put_cache("icon_" + this.server_unique_id + "_" + this.icon_id, response.clone(), "image/" + media); - - this.loaded_url = await response_to_url(response); - this.status = "loaded"; - this.triggerStatusChange(); - } - - set_error(error: string) { - if(this.status === "loaded" || this.status === "destroyed") return; - if(this.status === "error" && this.error_message === error) return; - this.status = "error"; - this.error_message = error; - this.triggerStatusChange(); - } - - async await_loading() { - await new Promise(resolve => { - if(this.status !== "loading") { - resolve(); - return; - } - const callback = () => { - if(this.status === "loading") return; - - this.status_change_callbacks.remove(callback); - resolve(); - }; - this.status_change_callbacks.push(callback); - }) - } -} - -async function response_to_url(response: Response) { - if(!response.headers.has('X-media-bytes')) - throw "missing media bytes"; - - const type = image_type(response.headers.get('X-media-bytes')); - const media = media_image_type(type); - - const blob = await response.blob(); - if(blob.type !== "image/" + media) - return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media)); - else - return URL.createObjectURL(blob) -} - -class CachedIconManager { - private loaded_icons: {[id: string]:LocalIcon} = {}; - - async clear_cache() { - await icon_cache.reset(); - this.clear_memory_cache(); - } - - clear_memory_cache() { - for(const icon_id of Object.keys(this.loaded_icons)) - this.loaded_icons[icon_id]["destroy"](); - this.loaded_icons = {}; - } - - load_icon(id: number, server_unique_id: string, fallback_load?: IconLoader) : LocalIcon { - const cache_id = server_unique_id + "_" + (id >>> 0); - if(this.loaded_icons[cache_id]) return this.loaded_icons[cache_id]; - - return (this.loaded_icons[cache_id] = new LocalIcon(id >>> 0, server_unique_id, fallback_load)); - } - - async put_icon(id: number, server_unique_id: string, icon: Response) { - const cache_id = server_unique_id + "_" + (id >>> 0); - if(this.loaded_icons[cache_id]) - await this.loaded_icons[cache_id].set_image(icon); - else { - const licon = this.loaded_icons[cache_id] = new LocalIcon(id >>> 0, server_unique_id, icon); - await new Promise((resolve, reject) => { - const cb = () => { - licon.status_change_callbacks.remove(cb); - if(licon.status === "loaded") - resolve(); - else - reject(licon.status === "error" ? licon.error_message || tr("Unknown error") : tr("Invalid status")); - }; - - licon.status_change_callbacks.push(cb); - }) - } - } -} -export const icon_cache_loader = new CachedIconManager(); -window.addEventListener("beforeunload", () => { - icon_cache_loader.clear_memory_cache(); -}); - -type IconManagerLoadingData = { - result: "success" | "error" | "unset"; - next_retry?: number; - error?: string; -} -export class IconManager { - handle: FileManager; - readonly events: Registry; - private loading_timestamps: {[key: number]: IconManagerLoadingData} = {}; - - constructor(handle: FileManager) { - this.handle = handle; - this.events = new Registry(); - } - - destroy() { - this.loading_timestamps = {}; - } - - async delete_icon(id: number) : Promise { - if(id <= 1000) - throw "invalid id!"; - - await this.handle.delete_file({ - name: '/icon_' + id - }); - } - - iconList() : Promise { - return this.handle.requestFileList("/icons"); - } - - create_icon_download(id: number) : Promise { - return this.handle.download_file("", "/icon_" + id); - } - - private async server_icon_loader(icon: LocalIcon) : Promise { - const loading_data: IconManagerLoadingData = this.loading_timestamps[icon.icon_id] || (this.loading_timestamps[icon.icon_id] = { result: "unset" }); - if(loading_data.result === "error") { - if(!loading_data.next_retry || loading_data.next_retry > Date.now()) { - log.debug(LogCategory.GENERAL, tr("Don't retry icon download from server. We'll try again in %s"), - !loading_data.next_retry ? tr("never") : format_time(loading_data.next_retry - Date.now(), tr("1 second"))); - throw loading_data.error; - } - } - - try { - let download_key: DownloadKey; - try { - download_key = await this.create_icon_download(icon.icon_id); - } catch(error) { - if(error instanceof CommandResult) { - if(error.id === ErrorID.FILE_NOT_FOUND) - throw tr("Icon could not be found"); - else if(error.id === ErrorID.PERMISSION_ERROR) - throw tr("No permissions to download icon"); - else - throw error.extra_message || error.message; - } - log.error(LogCategory.CLIENT, tr("Could not request download for icon %d: %o"), icon.icon_id, error); - throw typeof error === "string" ? error : tr("Failed to initialize icon download"); - } - - const downloader = spawn_download_transfer(download_key); - let response: Response; - try { - response = await downloader.request_file(); - } catch(error) { - log.error(LogCategory.CLIENT, tr("Could not download icon %d: %o"), icon.icon_id, error); - throw "failed to download icon"; - } - - loading_data.result = "success"; - return response; - } catch (error) { - loading_data.result = "error"; - loading_data.error = error as string; - loading_data.next_retry = Date.now() + 300 * 1000; - throw error; - } - } - - static generate_tag(icon: LocalIcon | undefined, options?: { - animate?: boolean - }) : JQuery { - options = options || {}; - - let icon_container = $.spawn("div").addClass("icon-container icon_empty"); - let icon_load_image = $.spawn("div").addClass("icon_loading"); - - const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", ""); - - if (icon.icon_id == 0) { - icon_load_image = undefined; - } else if (icon.icon_id < 1000) { - icon_load_image = undefined; - icon_container.removeClass("icon_empty").addClass("icon_em client-group_" + icon.icon_id); - } else { - const loading_done = sync => {//TODO: Show error? - if(icon.status === "empty") { - icon_load_image.remove(); - icon_load_image = undefined; - } else if(icon.status === "error") { - //TODO: Error icon? - icon_load_image.remove(); - icon_load_image = undefined; - } else { - icon_image.attr("src", icon.loaded_url); - icon_container.append(icon_image).removeClass("icon_empty"); - - if (!sync && (typeof (options.animate) !== "boolean" || options.animate)) { - icon_image.css("opacity", 0); - - icon_load_image.animate({opacity: 0}, 50, function () { - icon_load_image.remove(); - icon_image.animate({opacity: 1}, 150); - }); - } else { - icon_load_image.remove(); - icon_load_image = undefined; - } - } - }; - - if(icon.status !== "loading") - loading_done(true); - else { - const cb = () => { - if(icon.status === "loading") return; - - icon.status_change_callbacks.remove(cb); - loading_done(false); - }; - icon.status_change_callbacks.push(cb); - } - } - - if(icon_load_image) - icon_load_image.appendTo(icon_container); - return icon_container; - } - - generateTag(id: number, options?: { - animate?: boolean - }) : JQuery { - options = options || {}; - return IconManager.generate_tag(this.load_icon(id), options); - } - - load_icon(id: number) : LocalIcon { - const server_uid = this.handle.handle.channelTree.server.properties.virtualserver_unique_identifier; - let icon = icon_cache_loader.load_icon(id, server_uid, this.server_icon_loader.bind(this)); - if(icon.status !== "loading" && icon.status !== "loaded") { - this.server_icon_loader(icon).then(response => { - return icon.set_image(response); - }).catch(error => { - console.warn("Failed to update broken cached icon from server: %o", error); - }) - } - return icon; - } -} - -export class Avatar { - client_avatar_id: string; /* the base64 uid thing from a-m */ - avatar_id: string; /* client_flag_avatar */ - url: string; - type: ImageType; -} - -export class AvatarManager { - handle: FileManager; - - private static cache: CacheManager; - private _cached_avatars: {[response_avatar_id:number]:Avatar} = {}; - private _loading_promises: {[response_avatar_id:number]:Promise} = {}; - - constructor(handle: FileManager) { - this.handle = handle; - - if(!AvatarManager.cache) - AvatarManager.cache = new CacheManager("avatars"); - } - - destroy() { - this._cached_avatars = undefined; - this._loading_promises = undefined; - } - - private async _response_url(response: Response, type: ImageType) : Promise { - if(!response.headers.has('X-media-bytes')) - throw "missing media bytes"; - - const media = media_image_type(type); - const blob = await response.blob(); - if(blob.type !== "image/" + media) - return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media)); - else - return URL.createObjectURL(blob); - } - - async resolved_cached?(client_avatar_id: string, avatar_version?: string) : Promise { - let avatar: Avatar = this._cached_avatars[avatar_version]; - if(avatar) { - if(typeof(avatar_version) !== "string" || avatar.avatar_id == avatar_version) - return avatar; - avatar = undefined; - } - - if(!AvatarManager.cache.setupped()) - await AvatarManager.cache.setup(); - - const response = await AvatarManager.cache.resolve_cached('avatar_' + client_avatar_id); //TODO age! - if(!response) - return undefined; - - let response_avatar_version = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined; - if(typeof(avatar_version) === "string" && response_avatar_version != avatar_version) - return undefined; - - const type = image_type(response.headers.get('X-media-bytes')); - return this._cached_avatars[client_avatar_id] = { - client_avatar_id: client_avatar_id, - avatar_id: avatar_version || response_avatar_version, - url: await this._response_url(response, type), - type: type - }; - } - - create_avatar_download(client_avatar_id: string) : Promise { - log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id); - return this.handle.download_file("", "/avatar_" + client_avatar_id); - } - - private async _load_avatar(client_avatar_id: string, avatar_version: string) { - try { - let download_key: DownloadKey; - try { - download_key = await this.create_avatar_download(client_avatar_id); - } catch(error) { - log.error(LogCategory.GENERAL, tr("Could not request download for avatar %s: %o"), client_avatar_id, error); - throw "failed to request avatar download"; - } - - const downloader = spawn_download_transfer(download_key); - let response: Response; - try { - response = await downloader.request_file(); - } catch(error) { - log.error(LogCategory.GENERAL, tr("Could not download avatar %s: %o"), client_avatar_id, error); - throw "failed to download avatar"; - } - - const type = image_type(response.headers.get('X-media-bytes')); - const media = media_image_type(type); - - await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, { - "X-avatar-version": avatar_version - }); - const url = await this._response_url(response.clone(), type); - - return this._cached_avatars[client_avatar_id] = { - client_avatar_id: client_avatar_id, - avatar_id: avatar_version, - url: url, - type: type - }; - } finally { - this._loading_promises[client_avatar_id] = undefined; - } - } - - /* loads an avatar by the avatar id and optional with the avatar version */ - load_avatar(client_avatar_id: string, avatar_version: string) : Promise { - return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_version)); - } - - generate_client_tag(client: ClientEntry) : JQuery { - return this.generate_tag(client.avatarId(), client.properties.client_flag_avatar); - } - - update_cache(client_avatar_id: string, avatar_id: string) { - const _cached: Avatar = this._cached_avatars[client_avatar_id]; - if(_cached) { - if(_cached.avatar_id === avatar_id) - return; /* cache is up2date */ - - log.info(LogCategory.GENERAL, tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), client_avatar_id, _cached.avatar_id, avatar_id); - delete this._cached_avatars[client_avatar_id]; - AvatarManager.cache.delete("avatar_" + client_avatar_id).catch(error => { - log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o: %o"), client_avatar_id, error); - }); - } else { - this.resolved_cached(client_avatar_id).then(avatar => { - if(avatar && avatar.avatar_id !== avatar_id) { - /* this time we ensured that its cached */ - this.update_cache(client_avatar_id, avatar_id); - } - }).catch(error => { - log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o (cache lookup failed): %o"), client_avatar_id, error); - }); - } - } - - generate_tag(client_avatar_id: string, avatar_id?: string, options?: { - callback_image?: (tag: JQuery) => any, - callback_avatar?: (avatar: Avatar) => any - }) : JQuery { - options = options || {}; - - let avatar_container = $.spawn("div"); - let avatar_image = $.spawn("img").attr("alt", tr("Client avatar")); - - let cached_avatar: Avatar = this._cached_avatars[client_avatar_id]; - if(avatar_id === "") { - avatar_container.append(this.generate_default_image()); - } else if(cached_avatar && cached_avatar.avatar_id == avatar_id) { - avatar_image.attr("src", cached_avatar.url); - avatar_container.append(avatar_image); - if(options.callback_image) - options.callback_image(avatar_image); - if(options.callback_avatar) - options.callback_avatar(cached_avatar); - } else { - let loader_image = $.spawn("img"); - loader_image.attr("src", "img/loading_image.svg").css("width", "75%"); - avatar_container.append(loader_image); - - (async () => { - let avatar: Avatar; - try { - avatar = await this.resolved_cached(client_avatar_id, avatar_id); - } catch(error) { - log.error(LogCategory.CLIENT, error); - } - - if(!avatar) - avatar = await this.load_avatar(client_avatar_id, avatar_id); - - if(!avatar) - throw "failed to load avatar"; - - if(options.callback_avatar) - options.callback_avatar(avatar); - - avatar_image.attr("src", avatar.url); - avatar_image.css("opacity", 0); - avatar_container.append(avatar_image); - loader_image.animate({opacity: 0}, 50, () => { - loader_image.remove(); - avatar_image.animate({opacity: 1}, 150, () => { - if(options.callback_image) - options.callback_image(avatar_image); - }); - }); - })().catch(reason => { - log.error(LogCategory.CLIENT, tr("Could not load avatar for id %s. Reason: %s"), client_avatar_id, reason); - //TODO Broken image - loader_image.addClass("icon client-warning").attr("tag", tr("Could not load avatar ") + client_avatar_id); - }) - } - - return avatar_container; - } - - unique_id_2_avatar_id(unique_id: string) { - function str2ab(str) { - let buf = new ArrayBuffer(str.length); // 2 bytes for each char - let bufView = new Uint8Array(buf); - for (let i=0, strLen = str.length; i= '0' && c <= '9') - offset = c.charCodeAt(0) - '0'.charCodeAt(0); - else if(c >= 'A' && c <= 'F') - offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A; - else if(c >= 'a' && c <= 'f') - offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A; - result += String.fromCharCode('a'.charCodeAt(0) + offset); - } - return result; - } catch (e) { //invalid base 64 (like music bot etc) - return undefined; - } - } - - private generate_default_image() : JQuery { - return $.spawn("img").attr("src", "img/style/avatar.png").css({width: '100%', height: '100%'}); - } - - generate_chat_tag(client: { id?: number; database_id?: number; }, client_unique_id: string, callback_loaded?: (successfully: boolean, error?: any) => any) : JQuery { - let client_handle; - if(typeof(client.id) == "number") - client_handle = this.handle.handle.channelTree.findClient(client.id); - if(!client_handle && typeof(client.id) == "number") { - client_handle = this.handle.handle.channelTree.find_client_by_dbid(client.database_id); - } - - if(client_handle && client_handle.clientUid() !== client_unique_id) - client_handle = undefined; - - const container = $.spawn("div").addClass("avatar"); - if(client_handle && !client_handle.properties.client_flag_avatar) - return container.append(this.generate_default_image()); - - - const avatar_id = client_handle ? client_handle.avatarId() : this.unique_id_2_avatar_id(client_unique_id); - if(avatar_id) { - if(this._cached_avatars[avatar_id]) { /* Test if we're may able to load the client avatar sync without a loading screen */ - const cache: Avatar = this._cached_avatars[avatar_id]; - log.debug(LogCategory.GENERAL, tr("Using cached avatar. ID: %o | Version: %o (Cached: %o)"), avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined, cache.avatar_id); - if(!client_handle || client_handle.properties.client_flag_avatar == cache.avatar_id) { - const image = $.spawn("img").attr("src", cache.url).css({width: '100%', height: '100%'}); - return container.append(image); - } - } - - const image_loading = $.spawn("img").attr("src", "img/loading_image.svg").css({width: '100%', height: '100%'}); - - /* lets actually load the avatar */ - (async () => { - let avatar: Avatar; - let loaded_image = this.generate_default_image(); - - log.debug(LogCategory.GENERAL, tr("Resolving avatar. ID: %o | Version: %o"), avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); - try { - //TODO: Cache if avatar load failed and try again in some minutes/may just even consider using the default avatar 'till restart - try { - avatar = await this.resolved_cached(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); - } catch(error) { - log.error(LogCategory.GENERAL, tr("Failed to use cached avatar: %o"), error); - } - - if(!avatar) - avatar = await this.load_avatar(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); - - if(!avatar) - throw "no avatar present!"; - - loaded_image = $.spawn("img").attr("src", avatar.url).css({width: '100%', height: '100%'}); - } catch(error) { - throw error; - } finally { - container.children().remove(); - container.append(loaded_image); - } - })().then(() => callback_loaded && callback_loaded(true)).catch(error => { - log.warn(LogCategory.CLIENT, tr("Failed to load chat avatar for client %s. Error: %o"), client_unique_id, error); - callback_loaded && callback_loaded(false, error); - }); - - image_loading.appendTo(container); - } else { - this.generate_default_image().appendTo(container); - } - - return container; - } -} \ No newline at end of file diff --git a/shared/js/file/Avatars.tsx b/shared/js/file/Avatars.tsx new file mode 100644 index 00000000..9015a24b --- /dev/null +++ b/shared/js/file/Avatars.tsx @@ -0,0 +1,317 @@ +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {ClientEntry} from "tc-shared/ui/client"; +import * as hex from "tc-shared/crypto/hex"; +import { + DownloadKey, + FileManager, transfer_provider +} from "tc-shared/file/FileManager"; +import {image_type, ImageCache, ImageType, media_image_type} from "tc-shared/file/ImageCache"; + +export class Avatar { + client_avatar_id: string; /* the base64 uid thing from a-m */ + avatar_id: string; /* client_flag_avatar */ + url: string; + type: ImageType; +} + +export class AvatarManager { + handle: FileManager; + + private static cache: ImageCache; + private _cached_avatars: {[response_avatar_id:number]:Avatar} = {}; + private _loading_promises: {[response_avatar_id:number]:Promise} = {}; + + constructor(handle: FileManager) { + this.handle = handle; + + if(!AvatarManager.cache) + AvatarManager.cache = new ImageCache("avatars"); + } + + destroy() { + this._cached_avatars = undefined; + this._loading_promises = undefined; + } + + private async _response_url(response: Response, type: ImageType) : Promise { + if(!response.headers.has('X-media-bytes')) + throw "missing media bytes"; + + const media = media_image_type(type); + const blob = await response.blob(); + if(blob.type !== "image/" + media) + return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media)); + else + return URL.createObjectURL(blob); + } + + async resolved_cached?(client_avatar_id: string, avatar_version?: string) : Promise { + let avatar: Avatar = this._cached_avatars[avatar_version]; + if(avatar) { + if(typeof(avatar_version) !== "string" || avatar.avatar_id == avatar_version) + return avatar; + avatar = undefined; + } + + if(!AvatarManager.cache.setupped()) + await AvatarManager.cache.setup(); + + const response = await AvatarManager.cache.resolve_cached('avatar_' + client_avatar_id); //TODO age! + if(!response) + return undefined; + + let response_avatar_version = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined; + if(typeof(avatar_version) === "string" && response_avatar_version != avatar_version) + return undefined; + + const type = image_type(response.headers.get('X-media-bytes')); + return this._cached_avatars[client_avatar_id] = { + client_avatar_id: client_avatar_id, + avatar_id: avatar_version || response_avatar_version, + url: await this._response_url(response, type), + type: type + }; + } + + create_avatar_download(client_avatar_id: string) : Promise { + log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id); + return this.handle.download_file("", "/avatar_" + client_avatar_id); + } + + private async _load_avatar(client_avatar_id: string, avatar_version: string) { + try { + let download_key: DownloadKey; + try { + download_key = await this.create_avatar_download(client_avatar_id); + } catch(error) { + log.error(LogCategory.GENERAL, tr("Could not request download for avatar %s: %o"), client_avatar_id, error); + throw "failed to request avatar download"; + } + + const downloader = transfer_provider().spawn_download_transfer(download_key); + let response: Response; + try { + response = await downloader.request_file(); + } catch(error) { + log.error(LogCategory.GENERAL, tr("Could not download avatar %s: %o"), client_avatar_id, error); + throw "failed to download avatar"; + } + + const type = image_type(response.headers.get('X-media-bytes')); + const media = media_image_type(type); + + await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, { + "X-avatar-version": avatar_version + }); + const url = await this._response_url(response.clone(), type); + + return this._cached_avatars[client_avatar_id] = { + client_avatar_id: client_avatar_id, + avatar_id: avatar_version, + url: url, + type: type + }; + } finally { + this._loading_promises[client_avatar_id] = undefined; + } + } + + /* loads an avatar by the avatar id and optional with the avatar version */ + load_avatar(client_avatar_id: string, avatar_version: string) : Promise { + return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_version)); + } + + generate_client_tag(client: ClientEntry) : JQuery { + return this.generate_tag(client.avatarId(), client.properties.client_flag_avatar); + } + + update_cache(client_avatar_id: string, avatar_id: string) { + const _cached: Avatar = this._cached_avatars[client_avatar_id]; + if(_cached) { + if(_cached.avatar_id === avatar_id) + return; /* cache is up2date */ + + log.info(LogCategory.GENERAL, tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), client_avatar_id, _cached.avatar_id, avatar_id); + delete this._cached_avatars[client_avatar_id]; + AvatarManager.cache.delete("avatar_" + client_avatar_id).catch(error => { + log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o: %o"), client_avatar_id, error); + }); + } else { + this.resolved_cached(client_avatar_id).then(avatar => { + if(avatar && avatar.avatar_id !== avatar_id) { + /* this time we ensured that its cached */ + this.update_cache(client_avatar_id, avatar_id); + } + }).catch(error => { + log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o (cache lookup failed): %o"), client_avatar_id, error); + }); + } + } + + generate_tag(client_avatar_id: string, avatar_id?: string, options?: { + callback_image?: (tag: JQuery) => any, + callback_avatar?: (avatar: Avatar) => any + }) : JQuery { + options = options || {}; + + let avatar_container = $.spawn("div"); + let avatar_image = $.spawn("img").attr("alt", tr("Client avatar")); + + let cached_avatar: Avatar = this._cached_avatars[client_avatar_id]; + if(avatar_id === "") { + avatar_container.append(this.generate_default_image()); + } else if(cached_avatar && cached_avatar.avatar_id == avatar_id) { + avatar_image.attr("src", cached_avatar.url); + avatar_container.append(avatar_image); + if(options.callback_image) + options.callback_image(avatar_image); + if(options.callback_avatar) + options.callback_avatar(cached_avatar); + } else { + let loader_image = $.spawn("img"); + loader_image.attr("src", "img/loading_image.svg").css("width", "75%"); + avatar_container.append(loader_image); + + (async () => { + let avatar: Avatar; + try { + avatar = await this.resolved_cached(client_avatar_id, avatar_id); + } catch(error) { + log.error(LogCategory.CLIENT, error); + } + + if(!avatar) + avatar = await this.load_avatar(client_avatar_id, avatar_id); + + if(!avatar) + throw "failed to load avatar"; + + if(options.callback_avatar) + options.callback_avatar(avatar); + + avatar_image.attr("src", avatar.url); + avatar_image.css("opacity", 0); + avatar_container.append(avatar_image); + loader_image.animate({opacity: 0}, 50, () => { + loader_image.remove(); + avatar_image.animate({opacity: 1}, 150, () => { + if(options.callback_image) + options.callback_image(avatar_image); + }); + }); + })().catch(reason => { + log.error(LogCategory.CLIENT, tr("Could not load avatar for id %s. Reason: %s"), client_avatar_id, reason); + //TODO Broken image + loader_image.addClass("icon client-warning").attr("tag", tr("Could not load avatar ") + client_avatar_id); + }) + } + + return avatar_container; + } + + unique_id_2_avatar_id(unique_id: string) { + function str2ab(str) { + let buf = new ArrayBuffer(str.length); // 2 bytes for each char + let bufView = new Uint8Array(buf); + for (let i=0, strLen = str.length; i= '0' && c <= '9') + offset = c.charCodeAt(0) - '0'.charCodeAt(0); + else if(c >= 'A' && c <= 'F') + offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A; + else if(c >= 'a' && c <= 'f') + offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A; + result += String.fromCharCode('a'.charCodeAt(0) + offset); + } + return result; + } catch (e) { //invalid base 64 (like music bot etc) + return undefined; + } + } + + private generate_default_image() : JQuery { + return $.spawn("img").attr("src", "img/style/avatar.png").css({width: '100%', height: '100%'}); + } + + generate_chat_tag(client: { id?: number; database_id?: number; }, client_unique_id: string, callback_loaded?: (successfully: boolean, error?: any) => any) : JQuery { + let client_handle; + if(typeof(client.id) == "number") + client_handle = this.handle.handle.channelTree.findClient(client.id); + if(!client_handle && typeof(client.id) == "number") { + client_handle = this.handle.handle.channelTree.find_client_by_dbid(client.database_id); + } + + if(client_handle && client_handle.clientUid() !== client_unique_id) + client_handle = undefined; + + const container = $.spawn("div").addClass("avatar"); + if(client_handle && !client_handle.properties.client_flag_avatar) + return container.append(this.generate_default_image()); + + + const avatar_id = client_handle ? client_handle.avatarId() : this.unique_id_2_avatar_id(client_unique_id); + if(avatar_id) { + if(this._cached_avatars[avatar_id]) { /* Test if we're may able to load the client avatar sync without a loading screen */ + const cache: Avatar = this._cached_avatars[avatar_id]; + log.debug(LogCategory.GENERAL, tr("Using cached avatar. ID: %o | Version: %o (Cached: %o)"), avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined, cache.avatar_id); + if(!client_handle || client_handle.properties.client_flag_avatar == cache.avatar_id) { + const image = $.spawn("img").attr("src", cache.url).css({width: '100%', height: '100%'}); + return container.append(image); + } + } + + const image_loading = $.spawn("img").attr("src", "img/loading_image.svg").css({width: '100%', height: '100%'}); + + /* lets actually load the avatar */ + (async () => { + let avatar: Avatar; + let loaded_image = this.generate_default_image(); + + log.debug(LogCategory.GENERAL, tr("Resolving avatar. ID: %o | Version: %o"), avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); + try { + //TODO: Cache if avatar load failed and try again in some minutes/may just even consider using the default avatar 'till restart + try { + avatar = await this.resolved_cached(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); + } catch(error) { + log.error(LogCategory.GENERAL, tr("Failed to use cached avatar: %o"), error); + } + + if(!avatar) + avatar = await this.load_avatar(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); + + if(!avatar) + throw "no avatar present!"; + + loaded_image = $.spawn("img").attr("src", avatar.url).css({width: '100%', height: '100%'}); + } catch(error) { + throw error; + } finally { + container.children().remove(); + container.append(loaded_image); + } + })().then(() => callback_loaded && callback_loaded(true)).catch(error => { + log.warn(LogCategory.CLIENT, tr("Failed to load chat avatar for client %s. Error: %o"), client_unique_id, error); + callback_loaded && callback_loaded(false, error); + }); + + image_loading.appendTo(container); + } else { + this.generate_default_image().appendTo(container); + } + + return container; + } +} \ No newline at end of file diff --git a/shared/js/file/FileManager.tsx b/shared/js/file/FileManager.tsx new file mode 100644 index 00000000..0324f58b --- /dev/null +++ b/shared/js/file/FileManager.tsx @@ -0,0 +1,433 @@ +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {ChannelEntry} from "tc-shared/ui/channel"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {ServerCommand} from "tc-shared/connection/ConnectionBase"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler"; +import {IconManager} from "tc-shared/file/Icons"; +import {AvatarManager} from "tc-shared/file/Avatars"; + +export class FileEntry { + name: string; + datetime: number; + type: number; + size: number; +} + +export class FileListRequest { + path: string; + entries: FileEntry[]; + + callback: (entries: FileEntry[]) => void; +} + +export interface TransferKey { + client_transfer_id: number; + server_transfer_id: number; + + key: string; + + file_path: string; + file_name: string; + + peer: { + hosts: string[], + port: number; + }; + + total_size: number; +} + +export interface UploadOptions { + name: string; + path: string; + + channel?: ChannelEntry; + channel_password?: string; + + size: number; + overwrite: boolean; +} + +export interface DownloadTransfer { + get_key() : DownloadKey; + + request_file() : Promise; +} + +export interface UploadTransfer { + get_key(): UploadKey; + + put_data(data: BlobPart | File) : Promise; +} + +export type DownloadKey = TransferKey; +export type UploadKey = TransferKey; + +export interface TransferProvider { + spawn_download_transfer(key: DownloadKey) : DownloadTransfer; + spawn_upload_transfer(key: UploadKey) : UploadTransfer; +} + +let transfer_provider_: TransferProvider = new class implements TransferProvider { + spawn_download_transfer(key: TransferKey): DownloadTransfer { + return new RequestFileDownload(key); + } + + spawn_upload_transfer(key: TransferKey): UploadTransfer { + return new RequestFileUpload(key); + } +}; + +export function transfer_provider() : TransferProvider { + return transfer_provider_; +} + +export function set_transfer_provider(provider: TransferProvider) { + transfer_provider_ = provider; +} + +export class RequestFileDownload implements DownloadTransfer { + readonly transfer_key: DownloadKey; + + constructor(key: DownloadKey) { + this.transfer_key = key; + } + + async request_file() : Promise { + return await this.try_fetch("https://" + this.transfer_key.peer.hosts[0] + ":" + this.transfer_key.peer.port); + } + + private async try_fetch(url: string) : Promise { + const response = await fetch(url, { + method: 'GET', + cache: "no-cache", + mode: 'cors', + headers: { + 'transfer-key': this.transfer_key.key, + 'download-name': this.transfer_key.file_name, + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Expose-Headers': '*' + } + }); + if(!response.ok) { + debugger; + throw (response.type == 'opaque' || response.type == 'opaqueredirect' ? "invalid cross origin flag! May target isn't a TeaSpeak server?" : response.statusText || "response is not ok"); + } + return response; + } + + get_key(): DownloadKey { + return this.transfer_key; + } +} + +export class RequestFileUpload implements UploadTransfer { + readonly transfer_key: UploadKey; + constructor(key: DownloadKey) { + this.transfer_key = key; + } + + get_key(): UploadKey { + return this.transfer_key; + } + + async put_data(data: BlobPart | File) : Promise { + const form_data = new FormData(); + + if(data instanceof File) { + if(data.size != this.transfer_key.total_size) + throw "invalid size"; + + form_data.append("file", data); + } else if(typeof(data) === "string") { + if(data.length != this.transfer_key.total_size) + throw "invalid size"; + form_data.append("file", new Blob([data], { type: "application/octet-stream" })); + } else { + const buffer = data as BufferSource; + if(buffer.byteLength != this.transfer_key.total_size) + throw "invalid size"; + + form_data.append("file", new Blob([buffer], { type: "application/octet-stream" })); + } + + await this.try_put(form_data, "https://" + this.transfer_key.peer.hosts[0] + ":" + this.transfer_key.peer.port); + } + + private async try_put(data: FormData, url: string) : Promise { + const response = await fetch(url, { + method: 'POST', + cache: "no-cache", + mode: 'cors', + body: data, + headers: { + 'transfer-key': this.transfer_key.key, + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Expose-Headers': '*' + } + }); + if(!response.ok) + throw (response.type == 'opaque' || response.type == 'opaqueredirect' ? "invalid cross origin flag! May target isn't a TeaSpeak server?" : response.statusText || "response is not ok"); + } +} + +export class FileManager extends AbstractCommandHandler { + handle: ConnectionHandler; + icons: IconManager; + avatars: AvatarManager; + + private listRequests: FileListRequest[] = []; + private pending_download_requests: DownloadKey[] = []; + private pending_upload_requests: UploadKey[] = []; + + private transfer_counter : number = 1; + + constructor(client: ConnectionHandler) { + super(client.serverConnection); + + this.handle = client; + this.icons = new IconManager(this); + this.avatars = new AvatarManager(this); + + this.connection.command_handler_boss().register_handler(this); + } + + destroy() { + if(this.connection) { + const hboss = this.connection.command_handler_boss(); + if(hboss) + hboss.unregister_handler(this); + } + + this.listRequests = undefined; + this.pending_download_requests = undefined; + this.pending_upload_requests = undefined; + + this.icons && this.icons.destroy(); + this.icons = undefined; + + this.avatars && this.avatars.destroy(); + this.avatars = undefined; + } + + handle_command(command: ServerCommand): boolean { + switch (command.command) { + case "notifyfilelist": + this.notifyFileList(command.arguments); + return true; + case "notifyfilelistfinished": + this.notifyFileListFinished(command.arguments); + return true; + case "notifystartdownload": + this.notifyStartDownload(command.arguments); + return true; + case "notifystartupload": + this.notifyStartUpload(command.arguments); + return true; + } + return false; + } + + + /******************************** File list ********************************/ + //TODO multiple requests (same path) + requestFileList(path: string, channel?: ChannelEntry, password?: string) : Promise { + const _this = this; + return new Promise((accept, reject) => { + let req = new FileListRequest(); + req.path = path; + req.entries = []; + req.callback = accept; + _this.listRequests.push(req); + + _this.handle.serverConnection.send_command("ftgetfilelist", {"path": path, "cid": (channel ? channel.channelId : "0"), "cpw": (password ? password : "")}).then(() => {}).catch(reason => { + _this.listRequests.remove(req); + if(reason instanceof CommandResult) { + if(reason.id == 0x0501) { + accept([]); //Empty result + return; + } + } + reject(reason); + }); + }); + } + + private notifyFileList(json) { + let entry : FileListRequest = undefined; + + for(let e of this.listRequests) { + if(e.path == json[0]["path"]){ + entry = e; + break; + } + } + + if(!entry) { + log.error(LogCategory.CLIENT, tr("Invalid file list entry. Path: %s"), json[0]["path"]); + return; + } + for(let e of (json as Array)) { + e.datetime = parseInt(e.datetime + ""); + e.size = parseInt(e.size + ""); + e.type = parseInt(e.type + ""); + entry.entries.push(e); + } + } + + private notifyFileListFinished(json) { + let entry : FileListRequest = undefined; + + for(let e of this.listRequests) { + if(e.path == json[0]["path"]){ + entry = e; + this.listRequests.remove(e); + break; + } + } + + if(!entry) { + log.error(LogCategory.CLIENT, tr("Invalid file list entry finish. Path: "), json[0]["path"]); + return; + } + entry.callback(entry.entries); + } + + + /******************************** File download/upload ********************************/ + download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise { + const transfer_data: DownloadKey = { + file_name: file, + file_path: path, + client_transfer_id: this.transfer_counter++ + } as any; + + this.pending_download_requests.push(transfer_data); + return new Promise((resolve, reject) => { + transfer_data["_callback"] = resolve; + this.handle.serverConnection.send_command("ftinitdownload", { + "path": path, + "name": file, + "cid": (channel ? channel.channelId : "0"), + "cpw": (password ? password : ""), + "clientftfid": transfer_data.client_transfer_id, + "seekpos": 0, + "proto": 1 + }, {process_result: false}).catch(reason => { + this.pending_download_requests.remove(transfer_data); + reject(reason); + }) + }); + } + + upload_file(options: UploadOptions) : Promise { + const transfer_data: UploadKey = { + file_path: options.path, + file_name: options.name, + client_transfer_id: this.transfer_counter++, + total_size: options.size + } as any; + + this.pending_upload_requests.push(transfer_data); + return new Promise((resolve, reject) => { + transfer_data["_callback"] = resolve; + this.handle.serverConnection.send_command("ftinitupload", { + "path": options.path, + "name": options.name, + "cid": (options.channel ? options.channel.channelId : "0"), + "cpw": options.channel_password || "", + "clientftfid": transfer_data.client_transfer_id, + "size": options.size, + "overwrite": options.overwrite, + "resume": false, + "proto": 1 + }).catch(reason => { + this.pending_upload_requests.remove(transfer_data); + reject(reason); + }) + }); + } + + private notifyStartDownload(json) { + json = json[0]; + + let clientftfid = parseInt(json["clientftfid"]); + let transfer: DownloadKey; + for(let e of this.pending_download_requests) + if(e.client_transfer_id == clientftfid) { + transfer = e; + break; + } + + transfer.server_transfer_id = parseInt(json["serverftfid"]); + transfer.key = json["ftkey"]; + transfer.total_size = json["size"]; + + transfer.peer = { + hosts: (json["ip"] || "").split(","), + port: parseInt(json["port"]) + }; + + if(transfer.peer.hosts.length == 0) + transfer.peer.hosts.push("0.0.0.0"); + + if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0') + transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host; + + (transfer["_callback"] as (val: DownloadKey) => void)(transfer); + this.pending_download_requests.remove(transfer); + } + + private notifyStartUpload(json) { + json = json[0]; + + let transfer: UploadKey; + let clientftfid = parseInt(json["clientftfid"]); + for(let e of this.pending_upload_requests) + if(e.client_transfer_id == clientftfid) { + transfer = e; + break; + } + + transfer.server_transfer_id = parseInt(json["serverftfid"]); + transfer.key = json["ftkey"]; + + transfer.peer = { + hosts: (json["ip"] || "").split(","), + port: parseInt(json["port"]) + }; + + if(transfer.peer.hosts.length == 0) + transfer.peer.hosts.push("0.0.0.0"); + + if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0') + transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host; + + (transfer["_callback"] as (val: UploadKey) => void)(transfer); + this.pending_upload_requests.remove(transfer); + } + + /** File management **/ + async delete_file(props: { + name: string, + path?: string; + cid?: number; + cpw?: string; + }) : Promise { + if(!props.name) + throw "invalid name!"; + + try { + await this.handle.serverConnection.send_command("ftdeletefile", { + cid: props.cid || 0, + cpw: props.cpw, + path: props.path || "", + name: props.name + }) + } catch(error) { + throw error; + } + } +} \ No newline at end of file diff --git a/shared/js/file/Icons.tsx b/shared/js/file/Icons.tsx new file mode 100644 index 00000000..e9bd6df6 --- /dev/null +++ b/shared/js/file/Icons.tsx @@ -0,0 +1,379 @@ +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {Registry} from "tc-shared/events"; +import {format_time} from "tc-shared/ui/frames/chat"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import { + DownloadKey, + FileEntry, + FileManager, transfer_provider +} from "tc-shared/file/FileManager"; +import {image_type, ImageCache, ImageType, media_image_type} from "tc-shared/file/ImageCache"; + +const icon_cache: ImageCache = new ImageCache("icons"); +export interface IconManagerEvents { + notify_icon_state_changed: { + icon_id: number, + server_unique_id: string, + + icon: LocalIcon + }, +} + +//TODO: Invalidate icon after certain time if loading has failed and try to redownload (only if an icon loader has been set!) +type IconLoader = (icon?: LocalIcon) => Promise; +export class LocalIcon { + readonly icon_id: number; + readonly server_unique_id: string; + readonly status_change_callbacks: ((icon?: LocalIcon) => void)[] = []; + + status: "loading" | "loaded" | "empty" | "error" | "destroyed"; + + loaded_url?: string; + error_message?: string; + + private callback_icon_loader: IconLoader; + + constructor(id: number, server: string, loader_or_response: Response | IconLoader | undefined) { + this.icon_id = id; + this.server_unique_id = server; + + if(id >= 0 && id <= 1000) { + /* Internal TeaSpeak icons. These must be handled differently! */ + this.status = "loaded"; + } else { + this.status = "loading"; + if(loader_or_response instanceof Response) { + this.set_image(loader_or_response).catch(error => { + log.error(LogCategory.GENERAL, tr("Icon set image method threw an unexpected error: %o"), error); + this.status = "error"; + this.error_message = "unexpected parse error"; + this.triggerStatusChange(); + }); + } else { + this.callback_icon_loader = loader_or_response; + this.load().catch(error => { + log.error(LogCategory.GENERAL, tr("Icon load method threw an unexpected error: %o"), error); + this.status = "error"; + this.error_message = "unexpected load error"; + this.triggerStatusChange(); + }).then(() => { + this.callback_icon_loader = undefined; /* release resources captured by possible closures */ + }); + } + } + } + + private triggerStatusChange() { + for(const lister of this.status_change_callbacks.slice(0)) + lister(this); + } + + /* called within the CachedIconManager */ + protected destroy() { + if(typeof this.loaded_url === "string" && URL.revokeObjectURL) + URL.revokeObjectURL(this.loaded_url); + + this.status = "destroyed"; + this.loaded_url = undefined; + this.error_message = undefined; + + this.triggerStatusChange(); + this.status_change_callbacks.splice(0, this.status_change_callbacks.length); + } + + private async load() { + if(!icon_cache.setupped()) + await icon_cache.setup(); + + let response = await icon_cache.resolve_cached("icon_" + this.server_unique_id + "_" + this.icon_id); //TODO age! + if(!response) { + if(typeof this.callback_icon_loader !== "function") { + this.status = "empty"; + this.triggerStatusChange(); + return; + } + + try { + response = await this.callback_icon_loader(this); + } catch (error) { + log.warn(LogCategory.GENERAL, tr("Failed to download icon %d: %o"), this.icon_id, error); + await this.set_error(typeof error === "string" ? error : tr("Failed to load icon")); + return; + } + try { + await this.set_image(response); + } catch (error) { + log.error(LogCategory.GENERAL, tr("Failed to update icon image for icon %d: %o"), this.icon_id, error); + await this.set_error(typeof error === "string" ? error : tr("Failed to update icon from downloaded file")); + return; + } + return; + } + + this.loaded_url = await response_to_url(response); + this.status = "loaded"; + this.triggerStatusChange(); + } + + async set_image(response: Response) { + if(this.icon_id >= 0 && this.icon_id <= 1000) throw "Could not set image for internal icon"; + + const type = image_type(response.headers.get('X-media-bytes')); + if(type === ImageType.UNKNOWN) throw "unknown image type"; + + const media = media_image_type(type); + await icon_cache.put_cache("icon_" + this.server_unique_id + "_" + this.icon_id, response.clone(), "image/" + media); + + this.loaded_url = await response_to_url(response); + this.status = "loaded"; + this.triggerStatusChange(); + } + + set_error(error: string) { + if(this.status === "loaded" || this.status === "destroyed") return; + if(this.status === "error" && this.error_message === error) return; + this.status = "error"; + this.error_message = error; + this.triggerStatusChange(); + } + + async await_loading() { + await new Promise(resolve => { + if(this.status !== "loading") { + resolve(); + return; + } + const callback = () => { + if(this.status === "loading") return; + + this.status_change_callbacks.remove(callback); + resolve(); + }; + this.status_change_callbacks.push(callback); + }) + } +} + +async function response_to_url(response: Response) { + if(!response.headers.has('X-media-bytes')) + throw "missing media bytes"; + + const type = image_type(response.headers.get('X-media-bytes')); + const media = media_image_type(type); + + const blob = await response.blob(); + if(blob.type !== "image/" + media) + return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media)); + else + return URL.createObjectURL(blob) +} + +class CachedIconManager { + private loaded_icons: {[id: string]:LocalIcon} = {}; + + async clear_cache() { + await icon_cache.reset(); + this.clear_memory_cache(); + } + + clear_memory_cache() { + for(const icon_id of Object.keys(this.loaded_icons)) + this.loaded_icons[icon_id]["destroy"](); + this.loaded_icons = {}; + } + + load_icon(id: number, server_unique_id: string, fallback_load?: IconLoader) : LocalIcon { + const cache_id = server_unique_id + "_" + (id >>> 0); + if(this.loaded_icons[cache_id]) return this.loaded_icons[cache_id]; + + return (this.loaded_icons[cache_id] = new LocalIcon(id >>> 0, server_unique_id, fallback_load)); + } + + async put_icon(id: number, server_unique_id: string, icon: Response) { + const cache_id = server_unique_id + "_" + (id >>> 0); + if(this.loaded_icons[cache_id]) + await this.loaded_icons[cache_id].set_image(icon); + else { + const licon = this.loaded_icons[cache_id] = new LocalIcon(id >>> 0, server_unique_id, icon); + await new Promise((resolve, reject) => { + const cb = () => { + licon.status_change_callbacks.remove(cb); + if(licon.status === "loaded") + resolve(); + else + reject(licon.status === "error" ? licon.error_message || tr("Unknown error") : tr("Invalid status")); + }; + + licon.status_change_callbacks.push(cb); + }) + } + } +} +export const icon_cache_loader = new CachedIconManager(); +window.addEventListener("beforeunload", () => { + icon_cache_loader.clear_memory_cache(); +}); + +type IconManagerLoadingData = { + result: "success" | "error" | "unset"; + next_retry?: number; + error?: string; +} +export class IconManager { + handle: FileManager; + readonly events: Registry; + private loading_timestamps: {[key: number]: IconManagerLoadingData} = {}; + + constructor(handle: FileManager) { + this.handle = handle; + this.events = new Registry(); + } + + destroy() { + this.loading_timestamps = {}; + } + + async delete_icon(id: number) : Promise { + if(id <= 1000) + throw "invalid id!"; + + await this.handle.delete_file({ + name: '/icon_' + id + }); + } + + iconList() : Promise { + return this.handle.requestFileList("/icons"); + } + + create_icon_download(id: number) : Promise { + return this.handle.download_file("", "/icon_" + id); + } + + private async server_icon_loader(icon: LocalIcon) : Promise { + const loading_data: IconManagerLoadingData = this.loading_timestamps[icon.icon_id] || (this.loading_timestamps[icon.icon_id] = { result: "unset" }); + if(loading_data.result === "error") { + if(!loading_data.next_retry || loading_data.next_retry > Date.now()) { + log.debug(LogCategory.GENERAL, tr("Don't retry icon download from server. We'll try again in %s"), + !loading_data.next_retry ? tr("never") : format_time(loading_data.next_retry - Date.now(), tr("1 second"))); + throw loading_data.error; + } + } + + try { + let download_key: DownloadKey; + try { + download_key = await this.create_icon_download(icon.icon_id); + } catch(error) { + if(error instanceof CommandResult) { + if(error.id === ErrorID.FILE_NOT_FOUND) + throw tr("Icon could not be found"); + else if(error.id === ErrorID.PERMISSION_ERROR) + throw tr("No permissions to download icon"); + else + throw error.extra_message || error.message; + } + log.error(LogCategory.CLIENT, tr("Could not request download for icon %d: %o"), icon.icon_id, error); + throw typeof error === "string" ? error : tr("Failed to initialize icon download"); + } + + const downloader = transfer_provider().spawn_download_transfer(download_key); + let response: Response; + try { + response = await downloader.request_file(); + } catch(error) { + log.error(LogCategory.CLIENT, tr("Could not download icon %d: %o"), icon.icon_id, error); + throw "failed to download icon"; + } + + loading_data.result = "success"; + return response; + } catch (error) { + loading_data.result = "error"; + loading_data.error = error as string; + loading_data.next_retry = Date.now() + 300 * 1000; + throw error; + } + } + + static generate_tag(icon: LocalIcon | undefined, options?: { + animate?: boolean + }) : JQuery { + options = options || {}; + + let icon_container = $.spawn("div").addClass("icon-container icon_empty"); + let icon_load_image = $.spawn("div").addClass("icon_loading"); + + const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", ""); + + if (icon.icon_id == 0) { + icon_load_image = undefined; + } else if (icon.icon_id < 1000) { + icon_load_image = undefined; + icon_container.removeClass("icon_empty").addClass("icon_em client-group_" + icon.icon_id); + } else { + const loading_done = sync => {//TODO: Show error? + if(icon.status === "empty") { + icon_load_image.remove(); + icon_load_image = undefined; + } else if(icon.status === "error") { + //TODO: Error icon? + icon_load_image.remove(); + icon_load_image = undefined; + } else { + icon_image.attr("src", icon.loaded_url); + icon_container.append(icon_image).removeClass("icon_empty"); + + if (!sync && (typeof (options.animate) !== "boolean" || options.animate)) { + icon_image.css("opacity", 0); + + icon_load_image.animate({opacity: 0}, 50, function () { + icon_load_image.remove(); + icon_image.animate({opacity: 1}, 150); + }); + } else { + icon_load_image.remove(); + icon_load_image = undefined; + } + } + }; + + if(icon.status !== "loading") + loading_done(true); + else { + const cb = () => { + if(icon.status === "loading") return; + + icon.status_change_callbacks.remove(cb); + loading_done(false); + }; + icon.status_change_callbacks.push(cb); + } + } + + if(icon_load_image) + icon_load_image.appendTo(icon_container); + return icon_container; + } + + generateTag(id: number, options?: { + animate?: boolean + }) : JQuery { + options = options || {}; + return IconManager.generate_tag(this.load_icon(id), options); + } + + load_icon(id: number) : LocalIcon { + const server_uid = this.handle.handle.channelTree.server.properties.virtualserver_unique_identifier; + let icon = icon_cache_loader.load_icon(id, server_uid, this.server_icon_loader.bind(this)); + if(icon.status !== "loading" && icon.status !== "loaded") { + this.server_icon_loader(icon).then(response => { + return icon.set_image(response); + }).catch(error => { + console.warn("Failed to update broken cached icon from server: %o", error); + }) + } + return icon; + } +} \ No newline at end of file diff --git a/shared/js/file/ImageCache.ts b/shared/js/file/ImageCache.ts new file mode 100644 index 00000000..f8cdfdfe --- /dev/null +++ b/shared/js/file/ImageCache.ts @@ -0,0 +1,131 @@ + +export enum ImageType { + UNKNOWN, + BITMAP, + PNG, + GIF, + SVG, + JPEG +} + +export function media_image_type(type: ImageType, file?: boolean) { + switch (type) { + case ImageType.BITMAP: + return "bmp"; + case ImageType.GIF: + return "gif"; + case ImageType.SVG: + return file ? "svg" : "svg+xml"; + case ImageType.JPEG: + return "jpeg"; + case ImageType.UNKNOWN: + case ImageType.PNG: + default: + return "png"; + } +} + +export function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) { + const ab2str10 = () => { + const buf = new Uint8Array(encoded_data as ArrayBuffer); + if(buf.byteLength < 10) + return ""; + + let result = ""; + for(let index = 0; index < 10; index++) + result += String.fromCharCode(buf[index]); + return result; + }; + + const bin = typeof(encoded_data) === "string" ? ((typeof(base64_encoded) === "undefined" || base64_encoded) ? atob(encoded_data) : encoded_data) : ab2str10(); + if(bin.length < 10) return ImageType.UNKNOWN; + + if(bin[0] == String.fromCharCode(66) && bin[1] == String.fromCharCode(77)) { + return ImageType.BITMAP; + } else if(bin.substr(0, 8) == "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a") { + return ImageType.PNG; + } else if(bin.substr(0, 4) == "\x47\x49\x46\x38" && (bin[4] == '\x37' || bin[4] == '\x39') && bin[5] == '\x61') { + return ImageType.GIF; + } else if(bin[0] == '\x3c') { + return ImageType.SVG; + } else if(bin[0] == '\xFF' && bin[1] == '\xd8') { + return ImageType.JPEG; + } + + return ImageType.UNKNOWN; +} + +export class ImageCache { + readonly cache_name: string; + + private _cache_category: Cache; + + constructor(name: string) { + this.cache_name = name; + } + + setupped() : boolean { return !!this._cache_category; } + + async reset() { + if(!window.caches) + return; + + try { + await caches.delete(this.cache_name); + } catch(error) { + throw "Failed to delete cache: " + error; + } + try { + await this.setup(); + } catch(error) { + throw "Failed to reinitialize cache!"; + } + } + + async setup() { + if(!window.caches) + throw "Missing caches!"; + + this._cache_category = await caches.open(this.cache_name); + } + + async cleanup(max_age: number) { + /* FIXME: TODO */ + } + + async resolve_cached(key: string, max_age?: number) : Promise { + max_age = typeof(max_age) === "number" ? max_age : -1; + + const cached_response = await this._cache_category.match("https://_local_cache/cache_request_" + key); + if(!cached_response) + return undefined; + + /* FIXME: Max age */ + return cached_response; + } + + async put_cache(key: string, value: Response, type?: string, headers?: {[key: string]:string}) { + const new_headers = new Headers(); + for(const key of value.headers.keys()) + new_headers.set(key, value.headers.get(key)); + if(type) + new_headers.set("Content-type", type); + for(const key of Object.keys(headers || {})) + new_headers.set(key, headers[key]); + + await this._cache_category.put("https://_local_cache/cache_request_" + key, new Response(value.body, { + headers: new_headers + })); + } + + async delete(key: string) { + const flag = await this._cache_category.delete("https://_local_cache/cache_request_" + key, { + ignoreVary: true, + ignoreMethod: true, + ignoreSearch: true + }); + if(!flag) { + console.warn(tr("Failed to delete key %s from cache!"), flag); + } + } +} \ No newline at end of file diff --git a/shared/js/main.tsx b/shared/js/main.tsx index 11046d1e..84cebe3a 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -10,7 +10,7 @@ import * as i18n from "./i18n/localize"; import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {createInfoModal} from "tc-shared/ui/elements/Modal"; import {tra} from "./i18n/localize"; -import {RequestFileUpload} from "tc-shared/FileManager"; +import {RequestFileUpload} from "tc-shared/file/FileManager"; import * as stats from "./stats"; import * as fidentity from "./profiles/identities/TeaForumIdentity"; import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile"; diff --git a/shared/js/ui/frames/MenuBar.ts b/shared/js/ui/frames/MenuBar.ts index dee7a447..fe432670 100644 --- a/shared/js/ui/frames/MenuBar.ts +++ b/shared/js/ui/frames/MenuBar.ts @@ -1,4 +1,4 @@ -import {icon_cache_loader, IconManager, LocalIcon} from "tc-shared/FileManager"; +import {icon_cache_loader, IconManager, LocalIcon} from "tc-shared/file/FileManager"; import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks"; import { add_server_to_bookmarks, diff --git a/shared/js/ui/frames/control-bar/dropdown.tsx b/shared/js/ui/frames/control-bar/dropdown.tsx index d93c6703..2481f39c 100644 --- a/shared/js/ui/frames/control-bar/dropdown.tsx +++ b/shared/js/ui/frames/control-bar/dropdown.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase"; import {IconRenderer} from "tc-shared/ui/react-elements/Icon"; -import {LocalIcon} from "tc-shared/FileManager"; +import {LocalIcon} from "tc-shared/file/FileManager"; const cssStyle = require("./button.scss"); export interface DropdownEntryProperties { diff --git a/shared/js/ui/frames/control-bar/index.tsx b/shared/js/ui/frames/control-bar/index.tsx index 528c45d9..26b89f04 100644 --- a/shared/js/ui/frames/control-bar/index.tsx +++ b/shared/js/ui/frames/control-bar/index.tsx @@ -16,7 +16,7 @@ import { DirectoryBookmark, find_bookmark } from "tc-shared/bookmarks"; -import {icon_cache_loader, IconManager} from "tc-shared/FileManager"; +import {icon_cache_loader, IconManager} from "tc-shared/file/FileManager"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import {createInputModal} from "tc-shared/ui/elements/Modal"; import {default_recorder} from "tc-shared/voice/RecorderProfile"; diff --git a/shared/js/ui/modal/ModalAvatarList.ts b/shared/js/ui/modal/ModalAvatarList.ts index 8d3ac2e7..c072561e 100644 --- a/shared/js/ui/modal/ModalAvatarList.ts +++ b/shared/js/ui/modal/ModalAvatarList.ts @@ -2,7 +2,7 @@ import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal"; import {LogCategory} from "tc-shared/log"; import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {base64_encode_ab} from "tc-shared/utils/buffers"; -import {media_image_type} from "tc-shared/FileManager"; +import {media_image_type} from "tc-shared/file/FileManager"; import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import {ClientEntry} from "tc-shared/ui/client"; import * as log from "tc-shared/log"; diff --git a/shared/js/ui/modal/ModalBookmarks.ts b/shared/js/ui/modal/ModalBookmarks.ts index 27fba3a9..fbd17840 100644 --- a/shared/js/ui/modal/ModalBookmarks.ts +++ b/shared/js/ui/modal/ModalBookmarks.ts @@ -11,7 +11,7 @@ import { save_bookmark } from "tc-shared/bookmarks"; import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect"; -import {icon_cache_loader, IconManager} from "tc-shared/FileManager"; +import {icon_cache_loader, IconManager} from "tc-shared/file/FileManager"; import {profiles} from "tc-shared/profiles/ConnectionProfile"; import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import {Settings, settings} from "tc-shared/settings"; diff --git a/shared/js/ui/modal/ModalConnect.ts b/shared/js/ui/modal/ModalConnect.ts index ec941542..2746febc 100644 --- a/shared/js/ui/modal/ModalConnect.ts +++ b/shared/js/ui/modal/ModalConnect.ts @@ -5,7 +5,7 @@ import * as loader from "tc-loader"; import {createModal} from "tc-shared/ui/elements/Modal"; import {ConnectionProfile, default_profile, find_profile, profiles} from "tc-shared/profiles/ConnectionProfile"; import {KeyCode} from "tc-shared/PPTListener"; -import {icon_cache_loader, IconManager} from "tc-shared/FileManager"; +import {icon_cache_loader, IconManager} from "tc-shared/file/FileManager"; import * as i18nc from "tc-shared/i18n/country"; import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings"; import {server_connections} from "tc-shared/ui/frames/connection_handlers"; diff --git a/shared/js/ui/modal/ModalIconSelect.ts b/shared/js/ui/modal/ModalIconSelect.ts index 78cee825..dca9ca64 100644 --- a/shared/js/ui/modal/ModalIconSelect.ts +++ b/shared/js/ui/modal/ModalIconSelect.ts @@ -1,7 +1,7 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import PermissionType from "tc-shared/permission/PermissionType"; import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal"; -import {FileEntry, spawn_upload_transfer, UploadKey} from "tc-shared/FileManager"; +import {FileEntry, spawn_upload_transfer, UploadKey} from "tc-shared/file/FileManager"; import {LogCategory} from "tc-shared/log"; import * as log from "tc-shared/log"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; diff --git a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts index 34a56cc7..df7e8ff3 100644 --- a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts +++ b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts @@ -10,7 +10,7 @@ import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import {createInfoModal} from "tc-shared/ui/elements/Modal"; import {copy_to_clipboard} from "tc-shared/utils/helpers"; import PermissionType from "tc-shared/permission/PermissionType"; -import {icon_cache_loader, IconManager} from "tc-shared/FileManager"; +import {icon_cache_loader, IconManager} from "tc-shared/file/FileManager"; import {LogCategory} from "tc-shared/log"; import * as log from "tc-shared/log"; import { diff --git a/shared/js/ui/react-elements/Icon.tsx b/shared/js/ui/react-elements/Icon.tsx index 7e360cfd..7ea50ee1 100644 --- a/shared/js/ui/react-elements/Icon.tsx +++ b/shared/js/ui/react-elements/Icon.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import {LocalIcon} from "tc-shared/FileManager"; +import {LocalIcon} from "tc-shared/file/FileManager"; export interface IconProperties { icon: string | LocalIcon; diff --git a/shared/tsconfig/dtsconfig_app.json b/shared/tsconfig/dtsconfig_app.json index ef6184a4..fa89fdd9 100644 --- a/shared/tsconfig/dtsconfig_app.json +++ b/shared/tsconfig/dtsconfig_app.json @@ -1,6 +1,7 @@ { "source_files": [ - "**/*.ts" + "**/*.ts", + "**/*.tsx" ], "exclude": [ "workers/**/*.ts" diff --git a/tools/dtsgen/import_organizer.ts b/tools/dtsgen/import_organizer.ts index b498527d..998720eb 100644 --- a/tools/dtsgen/import_organizer.ts +++ b/tools/dtsgen/import_organizer.ts @@ -242,6 +242,14 @@ function analyze_type_node(node: ts.TypeNode | ts.LeftHandSideExpression, data: analyze_type_node(parenthesized.type, data); break; + case SyntaxKind.MappedType: + const mt = node as ts.MappedTypeNode; + analyze_type_node(mt.type, data); + break; + + case SyntaxKind.PropertyAccessExpression: + break; + default: throw "Unknown type " + SyntaxKind[node.kind] + ". Extend me :)"; }