Merge pull request #108 from tinode/audio-only

Audio-only calls
This commit is contained in:
Gene
2023-01-14 11:09:51 -08:00
committed by GitHub
31 changed files with 712 additions and 218 deletions
+32 -10
View File
@@ -96,6 +96,7 @@ html input[disabled] {
cursor: default; cursor: default;
opacity: 0.65; opacity: 0.65;
box-shadow: none; box-shadow: none;
filter: grayscale(50%);
} }
button.round { button.round {
@@ -117,7 +118,7 @@ button.round.small {
box-shadow: 0 2px 0.35rem 0 rgba(0, 0, 0, 0.25); box-shadow: 0 2px 0.35rem 0 rgba(0, 0, 0, 0.25);
} }
button:focus, button:hover, button:active { button:hover:not([disabled]), button:active:not([disabled]) {
color: #fff; color: #fff;
} }
button:active { button:active {
@@ -131,7 +132,7 @@ button.primary {
background-color: #1e88e5; /* blue 600 */ background-color: #1e88e5; /* blue 600 */
color: white; color: white;
} }
button.primary:focus, button.primary:hover, button.primary:active { button.primary:hover:not([disabled]), button.primary:active:not([disabled]) {
background-color: #2962ff; background-color: #2962ff;
} }
@@ -139,7 +140,7 @@ button.secondary {
background-color: #FFF; background-color: #FFF;
color: #2196f3; /* blue 500 */ color: #2196f3; /* blue 500 */
} }
button.secondary:focus, button.secondary:hover, button.secondary:active { button.secondary:hover:not([disabled]), button.secondary:active:not([disabled]) {
background-color: #e3f2fd; background-color: #e3f2fd;
} }
@@ -149,7 +150,7 @@ button.outline {
border: 1px solid silver; border: 1px solid silver;
box-shadow: none; box-shadow: none;
} }
button.outline:focus, button.outline:hover, button.outline:active { button.outline:hover:not([disabled]), button.outline:active:not([disabled]) {
background-color: #e8eaf6; /* indigo 50 */ background-color: #e8eaf6; /* indigo 50 */
} }
@@ -157,7 +158,7 @@ button.danger {
background-color: #ec6560; background-color: #ec6560;
color: white; color: white;
} }
button.danger:focus, button.danger:hover, button.danger:active { button.danger:hover:not([disabled]), button.danger:active:not([disabled]) {
background-color: #f77; background-color: #f77;
} }
@@ -165,7 +166,7 @@ button.positive {
background-color: #71c46f; background-color: #71c46f;
color: white; color: white;
} }
button.positive:focus, button.positive:hover, button.positive:active { button.positive:hover:not([disabled]), button.positive:active:not([disabled]) {
background-color: rgb(127, 237, 127); background-color: rgb(127, 237, 127);
} }
@@ -1605,13 +1606,26 @@ img.channel {
} }
.controls { .controls {
position: absolute;
display: flex; display: flex;
column-gap: 1rem;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 3.2rem; width: 100%;
bottom: 2.5rem;
z-index: 1; z-index: 1;
background-color: #eceff1; }
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
.incoming-call .controls {
bottom: 1rem;
}
.controls button {
border-radius: 50%;
width: 4rem;
height: 4rem;
min-width: initial;
box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.25);
} }
#video-container-panel { #video-container-panel {
@@ -1649,6 +1663,10 @@ img.channel {
z-index: 2; z-index: 2;
} }
.call-party.self[disabled] {
display: none;
}
video { video {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -1659,6 +1677,10 @@ video {
transform: scaleX(-1); transform: scaleX(-1);
} }
.call-party[disabled] video {
display: none;
}
.caller-card { .caller-card {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -1737,7 +1759,7 @@ video {
left: initial; left: initial;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 5rem 3.5rem 2rem 3.5rem; padding: 4rem 3.5rem 5.5rem 3.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
+1 -1
View File
File diff suppressed because one or more lines are too long
+64 -62
View File
@@ -1,12 +1,12 @@
{ {
"name": "tinode-webapp", "name": "tinode-webapp",
"version": "0.21.0-beta1", "version": "0.21.0-beta2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tinode-webapp", "name": "tinode-webapp",
"version": "0.21.0-beta1", "version": "0.21.0-beta2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"firebase": "^9.14.0", "firebase": "^9.14.0",
@@ -68,25 +68,25 @@
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.20.7", "version": "7.20.12",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.7.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz",
"integrity": "sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw==", "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.1.0", "@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.7", "@babel/generator": "^7.20.7",
"@babel/helper-compilation-targets": "^7.20.7", "@babel/helper-compilation-targets": "^7.20.7",
"@babel/helper-module-transforms": "^7.20.7", "@babel/helper-module-transforms": "^7.20.11",
"@babel/helpers": "^7.20.7", "@babel/helpers": "^7.20.7",
"@babel/parser": "^7.20.7", "@babel/parser": "^7.20.7",
"@babel/template": "^7.20.7", "@babel/template": "^7.20.7",
"@babel/traverse": "^7.20.7", "@babel/traverse": "^7.20.12",
"@babel/types": "^7.20.7", "@babel/types": "^7.20.7",
"convert-source-map": "^1.7.0", "convert-source-map": "^1.7.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
"json5": "^2.2.1", "json5": "^2.2.2",
"semver": "^6.3.0" "semver": "^6.3.0"
}, },
"engines": { "engines": {
@@ -170,9 +170,9 @@
} }
}, },
"node_modules/@babel/helper-create-class-features-plugin": { "node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.20.7", "version": "7.20.12",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz",
"integrity": "sha512-LtoWbDXOaidEf50hmdDqn9g8VEzsorMexoWMQdQODbvmqYmaF23pBP5VNPAGIFHsFQCIeKokDiz3CH5Y2jlY6w==", "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-annotate-as-pure": "^7.18.6",
@@ -181,6 +181,7 @@
"@babel/helper-member-expression-to-functions": "^7.20.7", "@babel/helper-member-expression-to-functions": "^7.20.7",
"@babel/helper-optimise-call-expression": "^7.18.6", "@babel/helper-optimise-call-expression": "^7.18.6",
"@babel/helper-replace-supers": "^7.20.7", "@babel/helper-replace-supers": "^7.20.7",
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
"@babel/helper-split-export-declaration": "^7.18.6" "@babel/helper-split-export-declaration": "^7.18.6"
}, },
"engines": { "engines": {
@@ -1698,9 +1699,9 @@
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.20.10", "version": "7.20.12",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.10.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz",
"integrity": "sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg==", "integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
@@ -3018,9 +3019,9 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "8.11.2", "version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
@@ -3096,9 +3097,9 @@
} }
}, },
"node_modules/babel-loader": { "node_modules/babel-loader": {
"version": "9.1.0", "version": "9.1.2",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.0.tgz", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz",
"integrity": "sha512-Antt61KJPinUMwHwIIz9T5zfMgevnfZkEVWYDWlG888fgdvRRGD0JTuf/fFozQnfT+uq64sk1bmdHDy/mOEWnA==", "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"find-cache-dir": "^3.3.2", "find-cache-dir": "^3.3.2",
@@ -3301,9 +3302,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001441", "version": "1.0.30001442",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz",
"integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", "integrity": "sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3935,9 +3936,9 @@
} }
}, },
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.14.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
"integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"reusify": "^1.0.4" "reusify": "^1.0.4"
@@ -4869,9 +4870,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.20", "version": "8.4.21",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -6392,9 +6393,9 @@
} }
}, },
"node_modules/webpack-cli/node_modules/commander": { "node_modules/webpack-cli/node_modules/commander": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
@@ -6650,25 +6651,25 @@
"dev": true "dev": true
}, },
"@babel/core": { "@babel/core": {
"version": "7.20.7", "version": "7.20.12",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.7.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz",
"integrity": "sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw==", "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@ampproject/remapping": "^2.1.0", "@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.7", "@babel/generator": "^7.20.7",
"@babel/helper-compilation-targets": "^7.20.7", "@babel/helper-compilation-targets": "^7.20.7",
"@babel/helper-module-transforms": "^7.20.7", "@babel/helper-module-transforms": "^7.20.11",
"@babel/helpers": "^7.20.7", "@babel/helpers": "^7.20.7",
"@babel/parser": "^7.20.7", "@babel/parser": "^7.20.7",
"@babel/template": "^7.20.7", "@babel/template": "^7.20.7",
"@babel/traverse": "^7.20.7", "@babel/traverse": "^7.20.12",
"@babel/types": "^7.20.7", "@babel/types": "^7.20.7",
"convert-source-map": "^1.7.0", "convert-source-map": "^1.7.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
"json5": "^2.2.1", "json5": "^2.2.2",
"semver": "^6.3.0" "semver": "^6.3.0"
} }
}, },
@@ -6729,9 +6730,9 @@
} }
}, },
"@babel/helper-create-class-features-plugin": { "@babel/helper-create-class-features-plugin": {
"version": "7.20.7", "version": "7.20.12",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz",
"integrity": "sha512-LtoWbDXOaidEf50hmdDqn9g8VEzsorMexoWMQdQODbvmqYmaF23pBP5VNPAGIFHsFQCIeKokDiz3CH5Y2jlY6w==", "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-annotate-as-pure": "^7.18.6",
@@ -6740,6 +6741,7 @@
"@babel/helper-member-expression-to-functions": "^7.20.7", "@babel/helper-member-expression-to-functions": "^7.20.7",
"@babel/helper-optimise-call-expression": "^7.18.6", "@babel/helper-optimise-call-expression": "^7.18.6",
"@babel/helper-replace-supers": "^7.20.7", "@babel/helper-replace-supers": "^7.20.7",
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
"@babel/helper-split-export-declaration": "^7.18.6" "@babel/helper-split-export-declaration": "^7.18.6"
} }
}, },
@@ -7771,9 +7773,9 @@
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.20.10", "version": "7.20.12",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.10.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz",
"integrity": "sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg==", "integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
@@ -8889,9 +8891,9 @@
"requires": {} "requires": {}
}, },
"ajv": { "ajv": {
"version": "8.11.2", "version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true, "dev": true,
"requires": { "requires": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
@@ -8943,9 +8945,9 @@
} }
}, },
"babel-loader": { "babel-loader": {
"version": "9.1.0", "version": "9.1.2",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.0.tgz", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz",
"integrity": "sha512-Antt61KJPinUMwHwIIz9T5zfMgevnfZkEVWYDWlG888fgdvRRGD0JTuf/fFozQnfT+uq64sk1bmdHDy/mOEWnA==", "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==",
"dev": true, "dev": true,
"requires": { "requires": {
"find-cache-dir": "^3.3.2", "find-cache-dir": "^3.3.2",
@@ -9082,9 +9084,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001441", "version": "1.0.30001442",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz",
"integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", "integrity": "sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==",
"dev": true "dev": true
}, },
"chalk": { "chalk": {
@@ -9545,9 +9547,9 @@
"dev": true "dev": true
}, },
"fastq": { "fastq": {
"version": "1.14.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
"integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
"dev": true, "dev": true,
"requires": { "requires": {
"reusify": "^1.0.4" "reusify": "^1.0.4"
@@ -10234,9 +10236,9 @@
} }
}, },
"postcss": { "postcss": {
"version": "8.4.20", "version": "8.4.21",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
"dev": true, "dev": true,
"requires": { "requires": {
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
@@ -11283,9 +11285,9 @@
}, },
"dependencies": { "dependencies": {
"commander": { "commander": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"dev": true "dev": true
} }
} }
+2 -1
View File
@@ -1,12 +1,13 @@
{ {
"name": "tinode-webapp", "name": "tinode-webapp",
"description": "Tinode messenger for the web", "description": "Tinode messenger for the web",
"version": "0.21.0-beta1", "version": "0.21.0-beta2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/tinode/example-react-js.git" "url": "https://github.com/tinode/example-react-js.git"
}, },
"scripts": { "scripts": {
"start": "echo 'Read installation instructions'",
"build": "npm run vers && npm run build:css && npm run extract && npm run build:i18n && npm run build:prod && npm run build:dev", "build": "npm run vers && npm run build:css && npm run extract && npm run build:i18n && npm run build:prod && npm run build:dev",
"clean": "rm umd/* && rm css/base.min.css && rm i18n.min/*", "clean": "rm umd/* && rm css/base.min.css && rm i18n.min/*",
"build:css": "postcss css/base.css > css/base.min.css", "build:css": "postcss css/base.css > css/base.min.css",
+5 -5
View File
@@ -64,16 +64,16 @@ const fbMessaging = firebase.messaging();
// This method shows the push notifications while the window is in background. // This method shows the push notifications while the window is in background.
fbMessaging.onBackgroundMessage(payload => { fbMessaging.onBackgroundMessage(payload => {
if (payload.data.silent == 'true') {
// TODO: if this is an 'msg', fetch the new message in the background.
return;
}
// Notify webapp that a message was received. // Notify webapp that a message was received.
if (webAppChannel) { if (webAppChannel) {
webAppChannel.postMessage(payload.data); webAppChannel.postMessage(payload.data);
} }
if (payload.data.silent == 'true') {
// No need to show anything.
return;
}
const titles = {'msg': 'new_message', 'sub': 'new_chat'}; const titles = {'msg': 'new_message', 'sub': 'new_chat'};
const pushType = payload.data.what || 'msg'; const pushType = payload.data.what || 'msg';
const title = payload.data.title || self.i18nMessage(titles[pushType]); const title = payload.data.title || self.i18nMessage(titles[pushType]);
+1 -1
View File
@@ -4,5 +4,5 @@ export const CALL_STATE_OUTGOING_INITATED = 1;
export const CALL_STATE_INCOMING_RECEIVED = 2; export const CALL_STATE_INCOMING_RECEIVED = 2;
export const CALL_STATE_IN_PROGRESS = 3; export const CALL_STATE_IN_PROGRESS = 3;
// Video call drafty headers representing call state. // Video call message headers representing call state.
export const CALL_HEAD_STARTED = 'started'; export const CALL_HEAD_STARTED = 'started';
+1 -1
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -1543,6 +1543,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_audio_call": {
"translation": "",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": true,
"obsolete": false
},
"unrecognized_video_format": { "unrecognized_video_format": {
"translation": "Das Format dieses Videos wird nicht erkannt", "translation": "Das Format dieses Videos wird nicht erkannt",
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
+7
View File
@@ -1543,6 +1543,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_audio_call": {
"translation": "Call",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": false,
"obsolete": false
},
"unrecognized_video_format": { "unrecognized_video_format": {
"translation": "Format of this video is not recognized", "translation": "Format of this video is not recognized",
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
+7
View File
@@ -1543,6 +1543,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_audio_call": {
"translation": "",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": true,
"obsolete": false
},
"unrecognized_video_format": { "unrecognized_video_format": {
"translation": "No se reconoce el formato de este video", "translation": "No se reconoce el formato de este video",
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
+4
View File
@@ -500,6 +500,10 @@
"defaultMessage": "Archive", "defaultMessage": "Archive",
"description": "Move topic from the list of active chats to archive" "description": "Move topic from the list of active chats to archive"
}, },
"menu_item_audio_call": {
"defaultMessage": "Call",
"description": "Start audio call"
},
"menu_item_block": { "menu_item_block": {
"defaultMessage": "Block", "defaultMessage": "Block",
"description": "Block topic or user" "description": "Block topic or user"
+7
View File
@@ -1543,6 +1543,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_audio_call": {
"translation": "",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": true,
"obsolete": false
},
"unrecognized_video_format": { "unrecognized_video_format": {
"translation": "Le format de cette vidéo n'est pas reconnu", "translation": "Le format de cette vidéo n'est pas reconnu",
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
+7
View File
@@ -1543,6 +1543,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_audio_call": {
"translation": "",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": true,
"obsolete": false
},
"unrecognized_video_format": { "unrecognized_video_format": {
"translation": "이 동영상의 형식을 인식할 수 없습니다.", "translation": "이 동영상의 형식을 인식할 수 없습니다.",
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
+7
View File
@@ -1543,6 +1543,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_audio_call": {
"translation": "",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": true,
"obsolete": false
},
"unrecognized_video_format": { "unrecognized_video_format": {
"translation": "Formatul acestui videoclip nu este recunoscut", "translation": "Formatul acestui videoclip nu este recunoscut",
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
+7
View File
@@ -1543,6 +1543,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_audio_call": {
"translation": "",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": true,
"obsolete": false
},
"unrecognized_video_format": { "unrecognized_video_format": {
"translation": "Неизвестный формат видео файла", "translation": "Неизвестный формат видео файла",
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
+7
View File
@@ -1543,6 +1543,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_audio_call": {
"translation": "",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": true,
"obsolete": false
},
"unrecognized_video_format": { "unrecognized_video_format": {
"translation": "無法識別此視頻的格式", "translation": "無法識別此視頻的格式",
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
+7
View File
@@ -1543,6 +1543,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_audio_call": {
"translation": "",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": true,
"obsolete": false
},
"unrecognized_video_format": { "unrecognized_video_format": {
"translation": "无法识别此视频的格式", "translation": "无法识别此视频的格式",
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit. // This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.21.0-beta1"; export const PACKAGE_VERSION = "0.21.0-beta2";
+3 -2
View File
@@ -303,8 +303,6 @@ class MessagesView extends React.Component {
scrollPosition: 0, scrollPosition: 0,
fetchingMessages: false, fetchingMessages: false,
showGoToLastButton: false, showGoToLastButton: false,
forwardMessage: null,
reply: null,
contentToEdit: null, contentToEdit: null,
dragging: false dragging: false
}; };
@@ -315,6 +313,8 @@ class MessagesView extends React.Component {
content: nextProps.forwardMessage.preview, content: nextProps.forwardMessage.preview,
seq: null seq: null
}; };
} else {
nextState.reply = null;
} }
if (topic) { if (topic) {
@@ -1358,6 +1358,7 @@ class MessagesView extends React.Component {
topic={this.state.topic} topic={this.state.topic}
seq={this.props.callSeq} seq={this.props.callSeq}
callState={this.props.callState} callState={this.props.callState}
callAudioOnly={this.props.callAudioOnly}
tinode={this.props.tinode} tinode={this.props.tinode}
title={this.state.title} title={this.state.title}
avatar={this.state.avatar || true} avatar={this.state.avatar || true}
+40 -12
View File
@@ -69,6 +69,11 @@ const messages = defineMessages({
defaultMessage: 'Info', defaultMessage: 'Info',
description: 'Show extended topic information' description: 'Show extended topic information'
}, },
menu_item_audio_call: {
id: 'menu_item_audio_call',
defaultMessage: 'Call',
description: 'Start audio call'
},
menu_item_video_call: { menu_item_video_call: {
id: 'menu_item_video_call', id: 'menu_item_video_call',
defaultMessage: 'Video call', defaultMessage: 'Video call',
@@ -158,6 +163,7 @@ class TinodeWeb extends React.Component {
this.handleHideForwardDialog = this.handleHideForwardDialog.bind(this); this.handleHideForwardDialog = this.handleHideForwardDialog.bind(this);
this.handleStartVideoCall = this.handleStartVideoCall.bind(this); this.handleStartVideoCall = this.handleStartVideoCall.bind(this);
this.handleStartAudioCall = this.handleStartAudioCall.bind(this);
this.handleInfoMessage = this.handleInfoMessage.bind(this); this.handleInfoMessage = this.handleInfoMessage.bind(this);
this.handleDataMessage = this.handleDataMessage.bind(this); this.handleDataMessage = this.handleDataMessage.bind(this);
this.handleCallClose = this.handleCallClose.bind(this); this.handleCallClose = this.handleCallClose.bind(this);
@@ -233,6 +239,7 @@ class TinodeWeb extends React.Component {
// Video calls. // Video calls.
callTopic: undefined, callTopic: undefined,
callState: CALL_STATE_NONE, callState: CALL_STATE_NONE,
callAudioOnly: undefined,
// If true, call state should be transitioned to CALL_STATE_IN_PROGRESS upon // If true, call state should be transitioned to CALL_STATE_IN_PROGRESS upon
// switching to the call topic. // switching to the call topic.
callShouldStart: false, callShouldStart: false,
@@ -717,20 +724,23 @@ class TinodeWeb extends React.Component {
this.handleLoginSuccessful(); this.handleLoginSuccessful();
} }
}).catch(err => { }).catch(err => {
const autoLogin = err.code >= 500;
// Connection or login failed, report error. // Connection or login failed, report error.
this.setState({ this.setState({
loginDisabled: false, loginDisabled: false,
credMethod: undefined, credMethod: undefined,
credCode: undefined, credCode: undefined,
loadSpinnerVisible: false, loadSpinnerVisible: false,
autoLogin: false autoLogin: autoLogin
}); });
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
console.warn("Login failed", err); console.warn("Login failed", err);
if (token) { if (!autoLogin) {
this.handleLogout(); if (token) {
this.handleLogout();
}
HashNavigation.navigateTo('');
} }
HashNavigation.navigateTo('');
}); });
} }
@@ -1589,6 +1599,10 @@ class TinodeWeb extends React.Component {
title: this.props.intl.formatMessage(messages.menu_item_info), title: this.props.intl.formatMessage(messages.menu_item_info),
handler: this.handleShowInfoView handler: this.handleShowInfoView
} : null, } : null,
subscribed && Tinode.isP2PTopicName(topicName) && webrtc ? {
title: this.props.intl.formatMessage(messages.menu_item_audio_call),
handler: this.handleStartAudioCall
} : null,
subscribed && Tinode.isP2PTopicName(topicName) && webrtc ? { subscribed && Tinode.isP2PTopicName(topicName) && webrtc ? {
title: this.props.intl.formatMessage(messages.menu_item_video_call), title: this.props.intl.formatMessage(messages.menu_item_video_call),
handler: this.handleStartVideoCall handler: this.handleStartVideoCall
@@ -1714,15 +1728,24 @@ class TinodeWeb extends React.Component {
handleStartVideoCall() { handleStartVideoCall() {
this.setState({ this.setState({
callTopic: this.state.topicSelected, callTopic: this.state.topicSelected,
callState: CALL_STATE_OUTGOING_INITATED callState: CALL_STATE_OUTGOING_INITATED,
callAudioOnly: false
}); });
} }
handleCallInvite(callTopic, callSeq, callState) { handleStartAudioCall() {
this.setState({
callTopic: this.state.topicSelected,
callState: CALL_STATE_OUTGOING_INITATED,
callAudioOnly: true
});
}
handleCallInvite(callTopic, callSeq, callState, audioOnly) {
switch (callState) { switch (callState) {
case CALL_STATE_OUTGOING_INITATED: case CALL_STATE_OUTGOING_INITATED:
let head = { webrtc: CALL_HEAD_STARTED }; const head = { webrtc: CALL_HEAD_STARTED, aonly: !!audioOnly };
this.handleSendMessage(Drafty.videoCall(), undefined, undefined, head) this.handleSendMessage(Drafty.videoCall(audioOnly), undefined, undefined, head)
.then(ctrl => { .then(ctrl => {
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) { if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
this.handleCallClose(); this.handleCallClose();
@@ -1792,7 +1815,8 @@ class TinodeWeb extends React.Component {
} }
this.setState({ this.setState({
callTopic: undefined, callTopic: undefined,
callState: CALL_STATE_NONE callState: CALL_STATE_NONE,
callAudioOnly: undefined
}); });
} }
@@ -1825,7 +1849,8 @@ class TinodeWeb extends React.Component {
this.setState({ this.setState({
callTopic: null, callTopic: null,
callState: CALL_STATE_NONE, callState: CALL_STATE_NONE,
callSeq: null callSeq: null,
callAudioOnly: undefined
}); });
return; return;
} }
@@ -1858,7 +1883,8 @@ class TinodeWeb extends React.Component {
this.setState({ this.setState({
callTopic: data.topic, callTopic: data.topic,
callState: CALL_STATE_INCOMING_RECEIVED, callState: CALL_STATE_INCOMING_RECEIVED,
callSeq: data.seq callSeq: data.seq,
callAudioOnly: !!msg.head.aonly
}); });
} else { } else {
// Another call is either in progress or being established. // Another call is either in progress or being established.
@@ -1914,10 +1940,11 @@ class TinodeWeb extends React.Component {
{this.state.callTopic && this.state.callState == CALL_STATE_INCOMING_RECEIVED ? {this.state.callTopic && this.state.callState == CALL_STATE_INCOMING_RECEIVED ?
<CallIncoming <CallIncoming
tinode={this.tinode} tinode={this.tinode}
onClose={this.handleCallClose}
topic={this.state.callTopic} topic={this.state.callTopic}
seq={this.state.callSeq} seq={this.state.callSeq}
callState={this.state.callState} callState={this.state.callState}
audioOnly={this.state.callAudioOnly}
onClose={this.handleCallClose}
onRinging={this.handleCallRinging} onRinging={this.handleCallRinging}
onAcceptCall={this.handleCallAccept} onAcceptCall={this.handleCallAccept}
onReject={this.handleCallHangup} onReject={this.handleCallHangup}
@@ -2030,6 +2057,7 @@ class TinodeWeb extends React.Component {
callTopic={this.state.callTopic} callTopic={this.state.callTopic}
callSeq={this.state.callSeq} callSeq={this.state.callSeq}
callState={this.state.callState} callState={this.state.callState}
callAudioOnly={this.state.callAudioOnly}
onCallHangup={this.handleCallHangup} onCallHangup={this.handleCallHangup}
onCallInvite={this.handleCallInvite} onCallInvite={this.handleCallInvite}
+206 -39
View File
@@ -15,6 +15,9 @@ const CALL_ENDED_SOUND = new Audio('audio/call-end.m4a');
CALL_ENDED_SOUND.loop = true; CALL_ENDED_SOUND.loop = true;
const DIALING_SOUND = new Audio('audio/dialing.m4a'); const DIALING_SOUND = new Audio('audio/dialing.m4a');
const VIDEO_MUTED_EVENT = 'video:muted';
const VIDEO_UNMUTED_EVENT = 'video:unmuted';
const messages = defineMessages({ const messages = defineMessages({
already_in_call: { already_in_call: {
id: 'already_in_call', id: 'already_in_call',
@@ -30,16 +33,22 @@ class CallPanel extends React.PureComponent {
this.state = { this.state = {
localStream: undefined, localStream: undefined,
pc: undefined, pc: undefined,
dataChannel: undefined,
previousOnInfo: undefined, previousOnInfo: undefined,
waitingForPeer: false, waitingForPeer: false,
// If true, the client has received a remote SDP from the peer and has sent a local SDP to the peer. // If true, the client has received a remote SDP from the peer and has sent a local SDP to the peer.
callInitialSetupComplete: false callInitialSetupComplete: false,
audioOnly: props.callAudioOnly,
// Video mute/unmute in progress.
videoToggleInProgress: false,
// Indicates if the remote peer has informed us that their camera is on.
remoteVideoLive: false,
}; };
this.localStreamConstraints = { this.localStreamConstraints = {
audio: true, audio: true,
video: true video: !props.callAudioOnly
}; };
this.isOutgoingCall = props.callState == CALL_STATE_OUTGOING_INITATED; this.isOutgoingCall = props.callState == CALL_STATE_OUTGOING_INITATED;
@@ -77,10 +86,19 @@ class CallPanel extends React.PureComponent {
this.handleCloseClick = this.handleCloseClick.bind(this); this.handleCloseClick = this.handleCloseClick.bind(this);
this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this); this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
this.handleToggleMicClick = this.handleToggleMicClick.bind(this); this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
this.toggleMedia = this.toggleMedia.bind(this);
this.handleRemoteHangup = this.handleRemoteHangup.bind(this); this.handleRemoteHangup = this.handleRemoteHangup.bind(this);
this.handleVideoCallAccepted = this.handleVideoCallAccepted.bind(this); this.handleVideoCallAccepted = this.handleVideoCallAccepted.bind(this);
this.muteVideo = this.muteVideo.bind(this);
this.unmuteVideo = this.unmuteVideo.bind(this);
this.emptyVideoTrack = this.emptyVideoTrack.bind(this);
this.handleDataChannelEvent = this.handleDataChannelEvent.bind(this);
this.handleDataChannelError = this.handleDataChannelError.bind(this);
this.handleDataChannelMessage = this.handleDataChannelMessage.bind(this);
this.handleDataChannelOpen = this.handleDataChannelOpen.bind(this);
this.handleDataChannelClose = this.handleDataChannelClose.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -105,6 +123,13 @@ class CallPanel extends React.PureComponent {
const stream = this.state.localStream; const stream = this.state.localStream;
stream.getTracks().forEach(track => { stream.getTracks().forEach(track => {
pc.addTrack(track, stream); pc.addTrack(track, stream);
if (track.kind == 'video' && !this.localStreamConstraints.video) {
// This is an audio-only call.
// Remove dummy video track (placeholder remains).
track.stop();
stream.removeTrack(track);
}
}); });
} }
@@ -138,6 +163,16 @@ class CallPanel extends React.PureComponent {
} }
} }
// Creates an empty video track placeholder.
emptyVideoTrack() {
const width = 640;
const height = 480;
const canvas = Object.assign(document.createElement("canvas"), {width, height});
canvas.getContext('2d').fillRect(0, 0, width, height);
const stream = canvas.captureStream(0);
return Object.assign(stream.getVideoTracks()[0], {enabled: false});
}
start() { start() {
if (this.state.localStream) { if (this.state.localStream) {
this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info'); this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info');
@@ -146,7 +181,7 @@ class CallPanel extends React.PureComponent {
if (this.props.callState == CALL_STATE_IN_PROGRESS) { if (this.props.callState == CALL_STATE_IN_PROGRESS) {
// We apparently just accepted the call. // We apparently just accepted the call.
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState); this.props.onInvite(this.props.topic, this.props.seq, CALL_STATE_IN_PROGRESS, this.props.callAudioOnly);
return; return;
} }
@@ -154,13 +189,18 @@ class CallPanel extends React.PureComponent {
// Start local video. // Start local video.
navigator.mediaDevices.getUserMedia(this.localStreamConstraints) navigator.mediaDevices.getUserMedia(this.localStreamConstraints)
.then(stream => { .then(stream => {
if (!this.localStreamConstraints.video) {
// Starting an audio-only call. Create a dummy video track
// (so video can be enabled during the call if the user desires).
stream.addTrack(this.emptyVideoTrack());
}
this.setState({localStream: stream, waitingForPeer: true}); this.setState({localStream: stream, waitingForPeer: true});
this.localRef.current.srcObject = stream; this.localRef.current.srcObject = stream;
DIALING_SOUND.play(); DIALING_SOUND.play();
// Send call invitation. // Send call invitation.
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState); this.props.onInvite(this.props.topic, this.props.seq, this.props.callState, this.props.callAudioOnly);
}) })
.catch(this.handleGetUserMediaError); .catch(this.handleGetUserMediaError);
} }
@@ -183,7 +223,11 @@ class CallPanel extends React.PureComponent {
this.state.pc.onicegatheringstatechange = null; this.state.pc.onicegatheringstatechange = null;
this.state.pc.onnegotiationneeded = null; this.state.pc.onnegotiationneeded = null;
this.state.pc.onicecandidateerror = null; this.state.pc.onicecandidateerror = null;
this.state.pc.ondatachannel = null;
if (this.state.dataChannel) {
this.state.dataChannel.close();
}
this.state.pc.close(); this.state.pc.close();
} }
this.setState({pc: null, waitingForPeer: false}); this.setState({pc: null, waitingForPeer: false});
@@ -209,6 +253,42 @@ class CallPanel extends React.PureComponent {
el.src = ''; el.src = '';
} }
handleDataChannelError(error) {
console.error('data channel error', error);
}
handleDataChannelMessage(event) {
switch (event.data) {
case VIDEO_MUTED_EVENT:
this.setState({remoteVideoLive: false});
break;
case VIDEO_UNMUTED_EVENT:
this.setState({remoteVideoLive: true});
break;
default:
break;
}
}
handleDataChannelOpen(event) {
if (!this.state.audioOnly) {
event.target.send(VIDEO_UNMUTED_EVENT);
}
}
handleDataChannelClose() {
/* no action */
}
handleDataChannelEvent(event) {
const channel = event.channel;
channel.onerror = this.handleDataChannelError;
channel.onmessage = this.handleDataChannelMessage;
channel.onopen = this.handleDataChannelOpen;
channel.onclose = this.handleDataChannelClose;
this.setState({dataChannel: channel});
}
createPeerConnection() { createPeerConnection() {
const iceServers = this.props.tinode.getServerParam('iceServers', null); const iceServers = this.props.tinode.getServerParam('iceServers', null);
const pc = iceServers ? new RTCPeerConnection({iceServers: iceServers}) : new RTCPeerConnection(); const pc = iceServers ? new RTCPeerConnection({iceServers: iceServers}) : new RTCPeerConnection();
@@ -220,8 +300,17 @@ class CallPanel extends React.PureComponent {
pc.onnegotiationneeded = this.handleNegotiationNeededEvent; pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
pc.onicecandidateerror = this.handleIceCandidateErrorEvent; pc.onicecandidateerror = this.handleIceCandidateErrorEvent;
pc.ontrack = this.handleTrackEvent; pc.ontrack = this.handleTrackEvent;
pc.ondatachannel = this.handleDataChannelEvent;
// creating data channel events
const channel = pc.createDataChannel("events", {ordered: true});
channel.onerror = this.handleDataChannelError;
channel.onmessage = this.handleDataChannelMessage;
channel.onopen = this.handleDataChannelOpen;
channel.onclose = this.handleDataChannelClose;
this.setState({pc: pc, waitingForPeer: false}); this.setState({pc: pc, waitingForPeer: false});
return pc; return pc;
} }
@@ -233,7 +322,7 @@ class CallPanel extends React.PureComponent {
.then(_ => { .then(_ => {
this.setState({ callInitialSetupComplete: true }, _ => this.drainRemoteIceCandidatesCache()); this.setState({ callInitialSetupComplete: true }, _ => this.drainRemoteIceCandidatesCache());
}) })
.catch(this.reportError); .catch(err => this.reportError(err));
} }
reportError(err) { reportError(err) {
@@ -244,17 +333,18 @@ class CallPanel extends React.PureComponent {
return this.isOutgoingCall || this.state.callInitialSetupComplete; return this.isOutgoingCall || this.state.callInitialSetupComplete;
} }
handleNegotiationNeededEvent() { handleNegotiationNeededEvent(event) {
const pc = event.target;
if (!this.canSendOffer()) { if (!this.canSendOffer()) {
return; return;
} }
this.state.pc.createOffer().then(offer => { pc.createOffer().then(offer => {
return this.state.pc.setLocalDescription(offer); return pc.setLocalDescription(offer);
}) })
.then(_ => { .then(_ => {
this.props.onSendOffer(this.props.topic, this.props.seq, this.state.pc.localDescription.toJSON()); this.props.onSendOffer(this.props.topic, this.props.seq, pc.localDescription.toJSON());
}) })
.catch(this.reportError); .catch(err => this.reportError(err));
} }
handleIceCandidateErrorEvent(event) { handleIceCandidateErrorEvent(event) {
@@ -271,7 +361,12 @@ class CallPanel extends React.PureComponent {
const candidate = new RTCIceCandidate(info.payload); const candidate = new RTCIceCandidate(info.payload);
if (this.state.callInitialSetupComplete) { if (this.state.callInitialSetupComplete) {
this.state.pc.addIceCandidate(candidate) this.state.pc.addIceCandidate(candidate)
.catch(this.reportError); .catch(err => {
if (candidate.candidate) {
this.reportError(err);
}
console.warn("Error adding new ice candidate", candidate, err);
});
} else { } else {
this.remoteIceCandidatesCache.push(candidate); this.remoteIceCandidatesCache.push(candidate);
} }
@@ -280,7 +375,12 @@ class CallPanel extends React.PureComponent {
drainRemoteIceCandidatesCache() { drainRemoteIceCandidatesCache() {
this.remoteIceCandidatesCache.forEach(candidate => { this.remoteIceCandidatesCache.forEach(candidate => {
this.state.pc.addIceCandidate(candidate) this.state.pc.addIceCandidate(candidate)
.catch(this.reportError); .catch(err => {
if (candidate.candidate) {
this.reportError(err);
}
console.warn("Error adding cached ice candidate", candidate, err);
});
}); });
this.remoteIceCandidatesCache = []; this.remoteIceCandidatesCache = [];
} }
@@ -295,7 +395,7 @@ class CallPanel extends React.PureComponent {
} }
handleSignalingStateChangeEvent(event) { handleSignalingStateChangeEvent(event) {
if (this.state.pc.signalingState == 'closed') { if (event.target.signalingState == 'closed') {
this.handleCloseClick(); this.handleCloseClick();
} }
} }
@@ -307,23 +407,24 @@ class CallPanel extends React.PureComponent {
handleTrackEvent(event) { handleTrackEvent(event) {
// Remote video becomes available. // Remote video becomes available.
this.remoteRef.current.srcObject = event.streams[0]; this.remoteRef.current.srcObject = event.streams[0];
// Make sure we display the title (peer's name) over the remote video. // Make sure we display the title (peer's name) over the remote video.
this.forceUpdate(); this.forceUpdate();
} }
handleGetUserMediaError(e) { handleGetUserMediaError(e) {
console.error("Error opening camera and/or microphone", e);
switch(e.name) { switch(e.name) {
case 'NotFoundError': case 'NotFoundError':
// Cannot start the call b/c no camera and/or microphone found. // Cannot start the call b/c no camera and/or microphone found.
this.reportError(e.message); this.reportError(e);
break; break;
case 'SecurityError': case 'SecurityError':
case 'PermissionDeniedError': case 'PermissionDeniedError':
// Do nothing; this is the same as the user canceling the call. // Do nothing; this is the same as the user canceling the call.
break; break;
default: default:
this.reportError(e.message); this.reportError(e);
console.error("Error opening your camera and/or microphone:", e.message);
break; break;
} }
@@ -334,13 +435,20 @@ class CallPanel extends React.PureComponent {
handleVideoOfferMsg(info) { handleVideoOfferMsg(info) {
let localStream = null; let localStream = null;
const pc = this.createPeerConnection(); const pc = this.state.pc ? this.state.pc : this.createPeerConnection();
const desc = new RTCSessionDescription(info.payload); const desc = new RTCSessionDescription(info.payload);
pc.setRemoteDescription(desc).then(_ => { pc.setRemoteDescription(desc).then(_ => {
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints); return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
}) })
.then(stream => { .then(stream => {
let dummyVideo;
if (!this.localStreamConstraints.video) {
// Starting an audio-only call. Create an empty video track so
// so the user can enable the video during the call.
dummyVideo = this.emptyVideoTrack();
stream.addTrack(dummyVideo);
}
localStream = stream; localStream = stream;
this.localRef.current.srcObject = stream; this.localRef.current.srcObject = stream;
this.setState({localStream: stream}); this.setState({localStream: stream});
@@ -348,6 +456,11 @@ class CallPanel extends React.PureComponent {
localStream.getTracks().forEach(track => { localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream); pc.addTrack(track, localStream);
}); });
if (dummyVideo) {
dummyVideo.stop();
stream.removeTrack(dummyVideo);
}
}) })
.then(_ => { .then(_ => {
return pc.createAnswer(); return pc.createAnswer();
@@ -387,49 +500,103 @@ class CallPanel extends React.PureComponent {
this.props.onHangup(this.props.topic, this.props.seq); this.props.onHangup(this.props.topic, this.props.seq);
} }
// Mute/unmute audio/video (specified by kind). // Ends video track and turns off the camera.
toggleMedia(kind) { muteVideo() {
if (!this.state.pc || !this.state.dataChannel) {
return;
}
const stream = this.state.localStream; const stream = this.state.localStream;
stream.getTracks().forEach(track => { const t = stream.getVideoTracks()[0];
if (track.kind != kind) { t.enabled = false;
return; t.stop();
stream.removeTrack(t);
this.state.dataChannel.send(VIDEO_MUTED_EVENT);
this.setState({videoToggleInProgress: false});
}
unmuteVideo() {
if (!this.state.pc || !this.state.dataChannel) {
return;
}
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
// Will extract video track from stream and throw stream away,
// and replace video track in the media sender.
this.localRef.current.srcObject = null;
const sender = this.state.pc.getSenders().find(s => s.track.kind == 'video');
const track = stream.getVideoTracks()[0];
// Remote track from new stream.
stream.removeTrack(track);
// Add this track to the existing local stream.
this.state.localStream.addTrack(track);
return sender.replaceTrack(track);
})
.then(_ => {
this.localRef.current.srcObject = this.state.localStream;
this.state.dataChannel.send(VIDEO_UNMUTED_EVENT);
})
.catch(err => this.handleGetUserMediaError(err))
.finally(_ => { this.setState({videoToggleInProgress: false}); }); // Make sure we redraw the mute/unmute icons (e.g. camera -> camera_off).
}
handleToggleCameraClick() {
if (this.state.videoToggleInProgress) {
// Toggle currently in progress.
return;
}
const tracks = this.state.localStream.getVideoTracks();
this.setState({videoToggleInProgress: true}, _ => {
if (tracks && tracks.length > 0 && tracks[0].enabled && tracks[0].readyState == 'live') {
this.muteVideo();
} else {
this.unmuteVideo();
} }
track.enabled = !track.enabled; this.setState({audioOnly: !this.state.audioOnly});
}); });
}
handleToggleMicClick() {
const stream = this.state.localStream;
const t = stream.getAudioTracks()[0];
t.enabled = !t.enabled;
// Make sure we redraw the mute/unmute icons (e.g. mic -> mic_off). // Make sure we redraw the mute/unmute icons (e.g. mic -> mic_off).
this.forceUpdate(); this.forceUpdate();
} }
handleToggleCameraClick() {
this.toggleMedia('video');
}
handleToggleMicClick() {
this.toggleMedia('audio');
}
render() { render() {
const remoteActive = this.remoteRef.current && this.remoteRef.current.srcObject; const audioTracks = this.state.localStream && this.state.localStream.getAudioTracks();
const disabled = !(this.state.localStream && this.state.localStream.getTracks()); const videoTracks = !this.state.audioOnly && this.state.localStream && this.state.localStream.getVideoTracks();
const audioIcon = this.state.localStream && this.state.localStream.getAudioTracks()[0].enabled ? 'mic' : 'mic_off'; const disabled = !this.state.pc || !this.state.dataChannel || !(audioTracks && audioTracks[0]);
const videoIcon = this.state.localStream && this.state.localStream.getVideoTracks()[0].enabled ? 'videocam' : 'videocam_off'; const audioIcon = audioTracks && audioTracks[0] && audioTracks[0].enabled ? 'mic' : 'mic_off';
const videoIcon = videoTracks && videoTracks[0] && videoTracks[0].enabled && videoTracks[0].readyState == 'live' ? 'videocam' : 'videocam_off';
const peerTitle = clipStr(this.props.title, MAX_PEER_TITLE_LENGTH); const peerTitle = clipStr(this.props.title, MAX_PEER_TITLE_LENGTH);
const pulseAnimation = this.state.waitingForPeer ? ' pulse' : ''; const pulseAnimation = this.state.waitingForPeer ? ' pulse' : '';
let removeActive = false;
if (this.remoteRef.current && this.remoteRef.current.srcObject && this.state.remoteVideoLive) {
const rstream = this.remoteRef.current.srcObject;
if (rstream.getVideoTracks().length > 0) {
const t = rstream.getVideoTracks()[0];
removeActive = t.enabled && t.readyState == 'live';
}
}
return ( return (
<> <>
<div id="video-container"> <div id="video-container">
<div id="video-container-panel"> <div id="video-container-panel">
<div className="call-party self"> <div className="call-party self" disabled={this.state.audioOnly}>
<video ref={this.localRef} autoPlay muted playsInline /> <video ref={this.localRef} autoPlay muted playsInline />
<div className="caller-name inactive"> <div className="caller-name inactive">
<FormattedMessage id="calls_you_label" <FormattedMessage id="calls_you_label"
defaultMessage="You" description="Shown over the local video screen" /> defaultMessage="You" description="Shown over the local video screen" />
</div> </div>
</div> </div>
<div className="call-party peer"> <div className="call-party peer" disabled={!removeActive}>
<video ref={this.remoteRef} autoPlay playsInline /> <video ref={this.remoteRef} autoPlay playsInline />
{remoteActive ? {removeActive ?
<div className="caller-name inactive">{peerTitle}</div> : <div className="caller-name inactive">{peerTitle}</div> :
<div className={`caller-card${pulseAnimation}`}> <div className={`caller-card${pulseAnimation}`}>
<div className="avatar-box"> <div className="avatar-box">
+1 -1
View File
@@ -28,7 +28,7 @@ export default class CallStatus extends React.PureComponent {
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />; duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
break; break;
} }
} else if (this.props.callState == 'started' && !this.props.duration) { } else if (['accepted', 'started'].includes(this.props.callState) && !this.props.duration) {
duration = <FormattedMessage id="call_in_progress" defaultMessage="in progress" description="Label for call in progress" /> duration = <FormattedMessage id="call_in_progress" defaultMessage="in progress" description="Label for call in progress" />
} else { } else {
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>; duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
+1 -1
View File
File diff suppressed because one or more lines are too long
+236 -51
View File
@@ -3544,7 +3544,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "PACKAGE_VERSION": () => (/* binding */ PACKAGE_VERSION) /* harmony export */ "PACKAGE_VERSION": () => (/* binding */ PACKAGE_VERSION)
/* harmony export */ }); /* harmony export */ });
const PACKAGE_VERSION = "0.21.0-beta1"; const PACKAGE_VERSION = "0.21.0-beta2";
/***/ }), /***/ }),
@@ -5721,8 +5721,6 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
scrollPosition: 0, scrollPosition: 0,
fetchingMessages: false, fetchingMessages: false,
showGoToLastButton: false, showGoToLastButton: false,
forwardMessage: null,
reply: null,
contentToEdit: null, contentToEdit: null,
dragging: false dragging: false
}; };
@@ -5731,6 +5729,8 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
content: nextProps.forwardMessage.preview, content: nextProps.forwardMessage.preview,
seq: null seq: null
}; };
} else {
nextState.reply = null;
} }
if (topic) { if (topic) {
const subs = []; const subs = [];
@@ -6635,6 +6635,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
topic: this.state.topic, topic: this.state.topic,
seq: this.props.callSeq, seq: this.props.callSeq,
callState: this.props.callState, callState: this.props.callState,
callAudioOnly: this.props.callAudioOnly,
tinode: this.props.tinode, tinode: this.props.tinode,
title: this.state.title, title: this.state.title,
avatar: this.state.avatar || true, avatar: this.state.avatar || true,
@@ -7772,6 +7773,13 @@ const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
"value": "Info" "value": "Info"
}] }]
}, },
menu_item_audio_call: {
id: "menu_item_audio_call",
defaultMessage: [{
"type": 0,
"value": "Call"
}]
},
menu_item_video_call: { menu_item_video_call: {
id: "menu_item_video_call", id: "menu_item_video_call",
defaultMessage: [{ defaultMessage: [{
@@ -7859,6 +7867,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
this.handleShowForwardDialog = this.handleShowForwardDialog.bind(this); this.handleShowForwardDialog = this.handleShowForwardDialog.bind(this);
this.handleHideForwardDialog = this.handleHideForwardDialog.bind(this); this.handleHideForwardDialog = this.handleHideForwardDialog.bind(this);
this.handleStartVideoCall = this.handleStartVideoCall.bind(this); this.handleStartVideoCall = this.handleStartVideoCall.bind(this);
this.handleStartAudioCall = this.handleStartAudioCall.bind(this);
this.handleInfoMessage = this.handleInfoMessage.bind(this); this.handleInfoMessage = this.handleInfoMessage.bind(this);
this.handleDataMessage = this.handleDataMessage.bind(this); this.handleDataMessage = this.handleDataMessage.bind(this);
this.handleCallClose = this.handleCallClose.bind(this); this.handleCallClose = this.handleCallClose.bind(this);
@@ -7913,6 +7922,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
mobilePanel: 'sidepanel', mobilePanel: 'sidepanel',
callTopic: undefined, callTopic: undefined,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE, callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE,
callAudioOnly: undefined,
callShouldStart: false, callShouldStart: false,
contextMenuVisible: false, contextMenuVisible: false,
contextMenuBounds: null, contextMenuBounds: null,
@@ -8347,19 +8357,22 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
this.handleLoginSuccessful(); this.handleLoginSuccessful();
} }
}).catch(err => { }).catch(err => {
const autoLogin = err.code >= 500;
this.setState({ this.setState({
loginDisabled: false, loginDisabled: false,
credMethod: undefined, credMethod: undefined,
credCode: undefined, credCode: undefined,
loadSpinnerVisible: false, loadSpinnerVisible: false,
autoLogin: false autoLogin: autoLogin
}); });
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
console.warn("Login failed", err); console.warn("Login failed", err);
if (token) { if (!autoLogin) {
this.handleLogout(); if (token) {
this.handleLogout();
}
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_18__["default"].navigateTo('');
} }
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_18__["default"].navigateTo('');
}); });
} }
static navigateToCredentialsView(params) { static navigateToCredentialsView(params) {
@@ -9105,6 +9118,9 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
return [subscribed ? { return [subscribed ? {
title: this.props.intl.formatMessage(messages.menu_item_info), title: this.props.intl.formatMessage(messages.menu_item_info),
handler: this.handleShowInfoView handler: this.handleShowInfoView
} : null, subscribed && tinode_sdk__WEBPACK_IMPORTED_MODULE_4__.Tinode.isP2PTopicName(topicName) && webrtc ? {
title: this.props.intl.formatMessage(messages.menu_item_audio_call),
handler: this.handleStartAudioCall
} : null, subscribed && tinode_sdk__WEBPACK_IMPORTED_MODULE_4__.Tinode.isP2PTopicName(topicName) && webrtc ? { } : null, subscribed && tinode_sdk__WEBPACK_IMPORTED_MODULE_4__.Tinode.isP2PTopicName(topicName) && webrtc ? {
title: this.props.intl.formatMessage(messages.menu_item_video_call), title: this.props.intl.formatMessage(messages.menu_item_video_call),
handler: this.handleStartVideoCall handler: this.handleStartVideoCall
@@ -9213,16 +9229,25 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
handleStartVideoCall() { handleStartVideoCall() {
this.setState({ this.setState({
callTopic: this.state.topicSelected, callTopic: this.state.topicSelected,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED,
callAudioOnly: false
}); });
} }
handleCallInvite(callTopic, callSeq, callState) { handleStartAudioCall() {
this.setState({
callTopic: this.state.topicSelected,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED,
callAudioOnly: true
});
}
handleCallInvite(callTopic, callSeq, callState, audioOnly) {
switch (callState) { switch (callState) {
case _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED: case _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED:
let head = { const head = {
webrtc: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_HEAD_STARTED webrtc: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_HEAD_STARTED,
aonly: !!audioOnly
}; };
this.handleSendMessage(tinode_sdk__WEBPACK_IMPORTED_MODULE_4__.Drafty.videoCall(), undefined, undefined, head).then(ctrl => { this.handleSendMessage(tinode_sdk__WEBPACK_IMPORTED_MODULE_4__.Drafty.videoCall(audioOnly), undefined, undefined, head).then(ctrl => {
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) { if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
this.handleCallClose(); this.handleCallClose();
return; return;
@@ -9282,7 +9307,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
} }
this.setState({ this.setState({
callTopic: undefined, callTopic: undefined,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE,
callAudioOnly: undefined
}); });
} }
handleCallAccept(topicName) { handleCallAccept(topicName) {
@@ -9311,7 +9337,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
this.setState({ this.setState({
callTopic: null, callTopic: null,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE, callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE,
callSeq: null callSeq: null,
callAudioOnly: undefined
}); });
return; return;
} }
@@ -9337,7 +9364,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
this.setState({ this.setState({
callTopic: data.topic, callTopic: data.topic,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_INCOMING_RECEIVED, callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_INCOMING_RECEIVED,
callSeq: data.seq callSeq: data.seq,
callAudioOnly: !!msg.head.aonly
}); });
} else { } else {
this.handleCallHangup(data.topic, data.seq); this.handleCallHangup(data.topic, data.seq);
@@ -9380,10 +9408,11 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
onTopicSelected: this.handleStartTopicRequest onTopicSelected: this.handleStartTopicRequest
}) : null, this.state.callTopic && this.state.callState == _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_INCOMING_RECEIVED ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_call_incoming_jsx__WEBPACK_IMPORTED_MODULE_8__["default"], { }) : null, this.state.callTopic && this.state.callState == _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_INCOMING_RECEIVED ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_call_incoming_jsx__WEBPACK_IMPORTED_MODULE_8__["default"], {
tinode: this.tinode, tinode: this.tinode,
onClose: this.handleCallClose,
topic: this.state.callTopic, topic: this.state.callTopic,
seq: this.state.callSeq, seq: this.state.callSeq,
callState: this.state.callState, callState: this.state.callState,
audioOnly: this.state.callAudioOnly,
onClose: this.handleCallClose,
onRinging: this.handleCallRinging, onRinging: this.handleCallRinging,
onAcceptCall: this.handleCallAccept, onAcceptCall: this.handleCallAccept,
onReject: this.handleCallHangup onReject: this.handleCallHangup
@@ -9486,6 +9515,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
callTopic: this.state.callTopic, callTopic: this.state.callTopic,
callSeq: this.state.callSeq, callSeq: this.state.callSeq,
callState: this.state.callState, callState: this.state.callState,
callAudioOnly: this.state.callAudioOnly,
onCallHangup: this.handleCallHangup, onCallHangup: this.handleCallHangup,
onCallInvite: this.handleCallInvite, onCallInvite: this.handleCallInvite,
onCallSendOffer: this.handleCallSendOffer, onCallSendOffer: this.handleCallSendOffer,
@@ -10662,6 +10692,8 @@ RING_SOUND.loop = true;
const CALL_ENDED_SOUND = new Audio('audio/call-end.m4a'); const CALL_ENDED_SOUND = new Audio('audio/call-end.m4a');
CALL_ENDED_SOUND.loop = true; CALL_ENDED_SOUND.loop = true;
const DIALING_SOUND = new Audio('audio/dialing.m4a'); const DIALING_SOUND = new Audio('audio/dialing.m4a');
const VIDEO_MUTED_EVENT = 'video:muted';
const VIDEO_UNMUTED_EVENT = 'video:unmuted';
const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({ const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
already_in_call: { already_in_call: {
id: "already_in_call", id: "already_in_call",
@@ -10677,13 +10709,17 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
this.state = { this.state = {
localStream: undefined, localStream: undefined,
pc: undefined, pc: undefined,
dataChannel: undefined,
previousOnInfo: undefined, previousOnInfo: undefined,
waitingForPeer: false, waitingForPeer: false,
callInitialSetupComplete: false callInitialSetupComplete: false,
audioOnly: props.callAudioOnly,
videoToggleInProgress: false,
remoteVideoLive: false
}; };
this.localStreamConstraints = { this.localStreamConstraints = {
audio: true, audio: true,
video: true video: !props.callAudioOnly
}; };
this.isOutgoingCall = props.callState == _constants_js__WEBPACK_IMPORTED_MODULE_4__.CALL_STATE_OUTGOING_INITATED; this.isOutgoingCall = props.callState == _constants_js__WEBPACK_IMPORTED_MODULE_4__.CALL_STATE_OUTGOING_INITATED;
this.localRef = react__WEBPACK_IMPORTED_MODULE_0___default().createRef(); this.localRef = react__WEBPACK_IMPORTED_MODULE_0___default().createRef();
@@ -10712,9 +10748,16 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
this.handleCloseClick = this.handleCloseClick.bind(this); this.handleCloseClick = this.handleCloseClick.bind(this);
this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this); this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
this.handleToggleMicClick = this.handleToggleMicClick.bind(this); this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
this.toggleMedia = this.toggleMedia.bind(this);
this.handleRemoteHangup = this.handleRemoteHangup.bind(this); this.handleRemoteHangup = this.handleRemoteHangup.bind(this);
this.handleVideoCallAccepted = this.handleVideoCallAccepted.bind(this); this.handleVideoCallAccepted = this.handleVideoCallAccepted.bind(this);
this.muteVideo = this.muteVideo.bind(this);
this.unmuteVideo = this.unmuteVideo.bind(this);
this.emptyVideoTrack = this.emptyVideoTrack.bind(this);
this.handleDataChannelEvent = this.handleDataChannelEvent.bind(this);
this.handleDataChannelError = this.handleDataChannelError.bind(this);
this.handleDataChannelMessage = this.handleDataChannelMessage.bind(this);
this.handleDataChannelOpen = this.handleDataChannelOpen.bind(this);
this.handleDataChannelClose = this.handleDataChannelClose.bind(this);
} }
componentDidMount() { componentDidMount() {
const topic = this.props.tinode.getTopic(this.props.topic); const topic = this.props.tinode.getTopic(this.props.topic);
@@ -10735,6 +10778,10 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
const stream = this.state.localStream; const stream = this.state.localStream;
stream.getTracks().forEach(track => { stream.getTracks().forEach(track => {
pc.addTrack(track, stream); pc.addTrack(track, stream);
if (track.kind == 'video' && !this.localStreamConstraints.video) {
track.stop();
stream.removeTrack(track);
}
}); });
} }
onInfo(info) { onInfo(info) {
@@ -10765,23 +10812,39 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
break; break;
} }
} }
emptyVideoTrack() {
const width = 640;
const height = 480;
const canvas = Object.assign(document.createElement("canvas"), {
width,
height
});
canvas.getContext('2d').fillRect(0, 0, width, height);
const stream = canvas.captureStream(0);
return Object.assign(stream.getVideoTracks()[0], {
enabled: false
});
}
start() { start() {
if (this.state.localStream) { if (this.state.localStream) {
this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info'); this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info');
return; return;
} }
if (this.props.callState == _constants_js__WEBPACK_IMPORTED_MODULE_4__.CALL_STATE_IN_PROGRESS) { if (this.props.callState == _constants_js__WEBPACK_IMPORTED_MODULE_4__.CALL_STATE_IN_PROGRESS) {
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState); this.props.onInvite(this.props.topic, this.props.seq, _constants_js__WEBPACK_IMPORTED_MODULE_4__.CALL_STATE_IN_PROGRESS, this.props.callAudioOnly);
return; return;
} }
navigator.mediaDevices.getUserMedia(this.localStreamConstraints).then(stream => { navigator.mediaDevices.getUserMedia(this.localStreamConstraints).then(stream => {
if (!this.localStreamConstraints.video) {
stream.addTrack(this.emptyVideoTrack());
}
this.setState({ this.setState({
localStream: stream, localStream: stream,
waitingForPeer: true waitingForPeer: true
}); });
this.localRef.current.srcObject = stream; this.localRef.current.srcObject = stream;
DIALING_SOUND.play(); DIALING_SOUND.play();
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState); this.props.onInvite(this.props.topic, this.props.seq, this.props.callState, this.props.callAudioOnly);
}).catch(this.handleGetUserMediaError); }).catch(this.handleGetUserMediaError);
} }
stop() { stop() {
@@ -10801,6 +10864,10 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
this.state.pc.onicegatheringstatechange = null; this.state.pc.onicegatheringstatechange = null;
this.state.pc.onnegotiationneeded = null; this.state.pc.onnegotiationneeded = null;
this.state.pc.onicecandidateerror = null; this.state.pc.onicecandidateerror = null;
this.state.pc.ondatachannel = null;
if (this.state.dataChannel) {
this.state.dataChannel.close();
}
this.state.pc.close(); this.state.pc.close();
} }
this.setState({ this.setState({
@@ -10826,6 +10893,41 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
el.srcObject = null; el.srcObject = null;
el.src = ''; el.src = '';
} }
handleDataChannelError(error) {
console.error('data channel error', error);
}
handleDataChannelMessage(event) {
switch (event.data) {
case VIDEO_MUTED_EVENT:
this.setState({
remoteVideoLive: false
});
break;
case VIDEO_UNMUTED_EVENT:
this.setState({
remoteVideoLive: true
});
break;
default:
break;
}
}
handleDataChannelOpen(event) {
if (!this.state.audioOnly) {
event.target.send(VIDEO_UNMUTED_EVENT);
}
}
handleDataChannelClose() {}
handleDataChannelEvent(event) {
const channel = event.channel;
channel.onerror = this.handleDataChannelError;
channel.onmessage = this.handleDataChannelMessage;
channel.onopen = this.handleDataChannelOpen;
channel.onclose = this.handleDataChannelClose;
this.setState({
dataChannel: channel
});
}
createPeerConnection() { createPeerConnection() {
const iceServers = this.props.tinode.getServerParam('iceServers', null); const iceServers = this.props.tinode.getServerParam('iceServers', null);
const pc = iceServers ? new RTCPeerConnection({ const pc = iceServers ? new RTCPeerConnection({
@@ -10838,6 +10940,14 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
pc.onnegotiationneeded = this.handleNegotiationNeededEvent; pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
pc.onicecandidateerror = this.handleIceCandidateErrorEvent; pc.onicecandidateerror = this.handleIceCandidateErrorEvent;
pc.ontrack = this.handleTrackEvent; pc.ontrack = this.handleTrackEvent;
pc.ondatachannel = this.handleDataChannelEvent;
const channel = pc.createDataChannel("events", {
ordered: true
});
channel.onerror = this.handleDataChannelError;
channel.onmessage = this.handleDataChannelMessage;
channel.onopen = this.handleDataChannelOpen;
channel.onclose = this.handleDataChannelClose;
this.setState({ this.setState({
pc: pc, pc: pc,
waitingForPeer: false waitingForPeer: false
@@ -10850,7 +10960,7 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
this.setState({ this.setState({
callInitialSetupComplete: true callInitialSetupComplete: true
}, _ => this.drainRemoteIceCandidatesCache()); }, _ => this.drainRemoteIceCandidatesCache());
}).catch(this.reportError); }).catch(err => this.reportError(err));
} }
reportError(err) { reportError(err) {
this.props.onError(err.message, 'err'); this.props.onError(err.message, 'err');
@@ -10858,15 +10968,16 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
canSendOffer() { canSendOffer() {
return this.isOutgoingCall || this.state.callInitialSetupComplete; return this.isOutgoingCall || this.state.callInitialSetupComplete;
} }
handleNegotiationNeededEvent() { handleNegotiationNeededEvent(event) {
const pc = event.target;
if (!this.canSendOffer()) { if (!this.canSendOffer()) {
return; return;
} }
this.state.pc.createOffer().then(offer => { pc.createOffer().then(offer => {
return this.state.pc.setLocalDescription(offer); return pc.setLocalDescription(offer);
}).then(_ => { }).then(_ => {
this.props.onSendOffer(this.props.topic, this.props.seq, this.state.pc.localDescription.toJSON()); this.props.onSendOffer(this.props.topic, this.props.seq, pc.localDescription.toJSON());
}).catch(this.reportError); }).catch(err => this.reportError(err));
} }
handleIceCandidateErrorEvent(event) { handleIceCandidateErrorEvent(event) {
console.warn("ICE candidate error:", event); console.warn("ICE candidate error:", event);
@@ -10879,14 +10990,24 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
handleNewICECandidateMsg(info) { handleNewICECandidateMsg(info) {
const candidate = new RTCIceCandidate(info.payload); const candidate = new RTCIceCandidate(info.payload);
if (this.state.callInitialSetupComplete) { if (this.state.callInitialSetupComplete) {
this.state.pc.addIceCandidate(candidate).catch(this.reportError); this.state.pc.addIceCandidate(candidate).catch(err => {
if (candidate.candidate) {
this.reportError(err);
}
console.warn("Error adding new ice candidate", candidate, err);
});
} else { } else {
this.remoteIceCandidatesCache.push(candidate); this.remoteIceCandidatesCache.push(candidate);
} }
} }
drainRemoteIceCandidatesCache() { drainRemoteIceCandidatesCache() {
this.remoteIceCandidatesCache.forEach(candidate => { this.remoteIceCandidatesCache.forEach(candidate => {
this.state.pc.addIceCandidate(candidate).catch(this.reportError); this.state.pc.addIceCandidate(candidate).catch(err => {
if (candidate.candidate) {
this.reportError(err);
}
console.warn("Error adding cached ice candidate", candidate, err);
});
}); });
this.remoteIceCandidatesCache = []; this.remoteIceCandidatesCache = [];
} }
@@ -10899,7 +11020,7 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
} }
} }
handleSignalingStateChangeEvent(event) { handleSignalingStateChangeEvent(event) {
if (this.state.pc.signalingState == 'closed') { if (event.target.signalingState == 'closed') {
this.handleCloseClick(); this.handleCloseClick();
} }
} }
@@ -10909,27 +11030,32 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
this.forceUpdate(); this.forceUpdate();
} }
handleGetUserMediaError(e) { handleGetUserMediaError(e) {
console.error("Error opening camera and/or microphone", e);
switch (e.name) { switch (e.name) {
case 'NotFoundError': case 'NotFoundError':
this.reportError(e.message); this.reportError(e);
break; break;
case 'SecurityError': case 'SecurityError':
case 'PermissionDeniedError': case 'PermissionDeniedError':
break; break;
default: default:
this.reportError(e.message); this.reportError(e);
console.error("Error opening your camera and/or microphone:", e.message);
break; break;
} }
this.handleCloseClick(); this.handleCloseClick();
} }
handleVideoOfferMsg(info) { handleVideoOfferMsg(info) {
let localStream = null; let localStream = null;
const pc = this.createPeerConnection(); const pc = this.state.pc ? this.state.pc : this.createPeerConnection();
const desc = new RTCSessionDescription(info.payload); const desc = new RTCSessionDescription(info.payload);
pc.setRemoteDescription(desc).then(_ => { pc.setRemoteDescription(desc).then(_ => {
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints); return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
}).then(stream => { }).then(stream => {
let dummyVideo;
if (!this.localStreamConstraints.video) {
dummyVideo = this.emptyVideoTrack();
stream.addTrack(dummyVideo);
}
localStream = stream; localStream = stream;
this.localRef.current.srcObject = stream; this.localRef.current.srcObject = stream;
this.setState({ this.setState({
@@ -10938,6 +11064,10 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
localStream.getTracks().forEach(track => { localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream); pc.addTrack(track, localStream);
}); });
if (dummyVideo) {
dummyVideo.stop();
stream.removeTrack(dummyVideo);
}
}).then(_ => { }).then(_ => {
return pc.createAnswer(); return pc.createAnswer();
}).then(answer => { }).then(answer => {
@@ -10969,35 +11099,89 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
this.stop(); this.stop();
this.props.onHangup(this.props.topic, this.props.seq); this.props.onHangup(this.props.topic, this.props.seq);
} }
toggleMedia(kind) { muteVideo() {
if (!this.state.pc || !this.state.dataChannel) {
return;
}
const stream = this.state.localStream; const stream = this.state.localStream;
stream.getTracks().forEach(track => { const t = stream.getVideoTracks()[0];
if (track.kind != kind) { t.enabled = false;
return; t.stop();
} stream.removeTrack(t);
track.enabled = !track.enabled; this.state.dataChannel.send(VIDEO_MUTED_EVENT);
this.setState({
videoToggleInProgress: false
});
}
unmuteVideo() {
if (!this.state.pc || !this.state.dataChannel) {
return;
}
navigator.mediaDevices.getUserMedia({
video: true
}).then(stream => {
this.localRef.current.srcObject = null;
const sender = this.state.pc.getSenders().find(s => s.track.kind == 'video');
const track = stream.getVideoTracks()[0];
stream.removeTrack(track);
this.state.localStream.addTrack(track);
return sender.replaceTrack(track);
}).then(_ => {
this.localRef.current.srcObject = this.state.localStream;
this.state.dataChannel.send(VIDEO_UNMUTED_EVENT);
}).catch(err => this.handleGetUserMediaError(err)).finally(_ => {
this.setState({
videoToggleInProgress: false
});
}); });
this.forceUpdate();
} }
handleToggleCameraClick() { handleToggleCameraClick() {
this.toggleMedia('video'); if (this.state.videoToggleInProgress) {
return;
}
const tracks = this.state.localStream.getVideoTracks();
this.setState({
videoToggleInProgress: true
}, _ => {
if (tracks && tracks.length > 0 && tracks[0].enabled && tracks[0].readyState == 'live') {
this.muteVideo();
} else {
this.unmuteVideo();
}
this.setState({
audioOnly: !this.state.audioOnly
});
});
} }
handleToggleMicClick() { handleToggleMicClick() {
this.toggleMedia('audio'); const stream = this.state.localStream;
const t = stream.getAudioTracks()[0];
t.enabled = !t.enabled;
this.forceUpdate();
} }
render() { render() {
const remoteActive = this.remoteRef.current && this.remoteRef.current.srcObject; const audioTracks = this.state.localStream && this.state.localStream.getAudioTracks();
const disabled = !(this.state.localStream && this.state.localStream.getTracks()); const videoTracks = !this.state.audioOnly && this.state.localStream && this.state.localStream.getVideoTracks();
const audioIcon = this.state.localStream && this.state.localStream.getAudioTracks()[0].enabled ? 'mic' : 'mic_off'; const disabled = !this.state.pc || !this.state.dataChannel || !(audioTracks && audioTracks[0]);
const videoIcon = this.state.localStream && this.state.localStream.getVideoTracks()[0].enabled ? 'videocam' : 'videocam_off'; const audioIcon = audioTracks && audioTracks[0] && audioTracks[0].enabled ? 'mic' : 'mic_off';
const videoIcon = videoTracks && videoTracks[0] && videoTracks[0].enabled && videoTracks[0].readyState == 'live' ? 'videocam' : 'videocam_off';
const peerTitle = (0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_5__.clipStr)(this.props.title, _config_js__WEBPACK_IMPORTED_MODULE_3__.MAX_PEER_TITLE_LENGTH); const peerTitle = (0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_5__.clipStr)(this.props.title, _config_js__WEBPACK_IMPORTED_MODULE_3__.MAX_PEER_TITLE_LENGTH);
const pulseAnimation = this.state.waitingForPeer ? ' pulse' : ''; const pulseAnimation = this.state.waitingForPeer ? ' pulse' : '';
let removeActive = false;
if (this.remoteRef.current && this.remoteRef.current.srcObject && this.state.remoteVideoLive) {
const rstream = this.remoteRef.current.srcObject;
if (rstream.getVideoTracks().length > 0) {
const t = rstream.getVideoTracks()[0];
removeActive = t.enabled && t.readyState == 'live';
}
}
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { return react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "video-container" id: "video-container"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "video-container-panel" id: "video-container-panel"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "call-party self" className: "call-party self",
disabled: this.state.audioOnly
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("video", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("video", {
ref: this.localRef, ref: this.localRef,
autoPlay: true, autoPlay: true,
@@ -11012,12 +11196,13 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
"value": "You" "value": "You"
}] }]
}))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "call-party peer" className: "call-party peer",
disabled: !removeActive
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("video", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("video", {
ref: this.remoteRef, ref: this.remoteRef,
autoPlay: true, autoPlay: true,
playsInline: true playsInline: true
}), remoteActive ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }), removeActive ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "caller-name inactive" className: "caller-name inactive"
}, peerTitle) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }, peerTitle) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: `caller-card${pulseAnimation}` className: `caller-card${pulseAnimation}`
@@ -11116,7 +11301,7 @@ class CallStatus extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
}); });
break; break;
} }
} else if (this.props.callState == 'started' && !this.props.duration) { } else if (['accepted', 'started'].includes(this.props.callState) && !this.props.duration) {
duration = react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, { duration = react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "call_in_progress", id: "call_in_progress",
defaultMessage: [{ defaultMessage: [{
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+45 -24
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit. // This is a generated file. Don't edit.
const PACKAGE_VERSION = "0.21.0-beta1"; const PACKAGE_VERSION = "0.21.0-beta2";