Merge branch 'next' into devel
This commit is contained in:
+32
-10
@@ -96,6 +96,7 @@ html input[disabled] {
|
||||
cursor: default;
|
||||
opacity: 0.65;
|
||||
box-shadow: none;
|
||||
filter: grayscale(50%);
|
||||
}
|
||||
|
||||
button.round {
|
||||
@@ -117,7 +118,7 @@ button.round.small {
|
||||
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;
|
||||
}
|
||||
button:active {
|
||||
@@ -131,7 +132,7 @@ button.primary {
|
||||
background-color: #1e88e5; /* blue 600 */
|
||||
color: white;
|
||||
}
|
||||
button.primary:focus, button.primary:hover, button.primary:active {
|
||||
button.primary:hover:not([disabled]), button.primary:active:not([disabled]) {
|
||||
background-color: #2962ff;
|
||||
}
|
||||
|
||||
@@ -139,7 +140,7 @@ button.secondary {
|
||||
background-color: #FFF;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -149,7 +150,7 @@ button.outline {
|
||||
border: 1px solid silver;
|
||||
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 */
|
||||
}
|
||||
|
||||
@@ -157,7 +158,7 @@ button.danger {
|
||||
background-color: #ec6560;
|
||||
color: white;
|
||||
}
|
||||
button.danger:focus, button.danger:hover, button.danger:active {
|
||||
button.danger:hover:not([disabled]), button.danger:active:not([disabled]) {
|
||||
background-color: #f77;
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ button.positive {
|
||||
background-color: #71c46f;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1605,13 +1606,26 @@ img.channel {
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 3.2rem;
|
||||
width: 100%;
|
||||
bottom: 2.5rem;
|
||||
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 {
|
||||
@@ -1649,6 +1663,10 @@ img.channel {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.call-party.self[disabled] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -1659,6 +1677,10 @@ video {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.call-party[disabled] video {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.caller-card {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -1737,7 +1759,7 @@ video {
|
||||
left: initial;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 5rem 3.5rem 2rem 3.5rem;
|
||||
padding: 4rem 3.5rem 5.5rem 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Generated
+64
-62
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "tinode-webapp",
|
||||
"version": "0.21.0-beta1",
|
||||
"version": "0.21.0-beta2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tinode-webapp",
|
||||
"version": "0.21.0-beta1",
|
||||
"version": "0.21.0-beta2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^9.14.0",
|
||||
@@ -68,25 +68,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.7.tgz",
|
||||
"integrity": "sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw==",
|
||||
"version": "7.20.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz",
|
||||
"integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.1.0",
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^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/parser": "^7.20.7",
|
||||
"@babel/template": "^7.20.7",
|
||||
"@babel/traverse": "^7.20.7",
|
||||
"@babel/traverse": "^7.20.12",
|
||||
"@babel/types": "^7.20.7",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
"json5": "^2.2.1",
|
||||
"json5": "^2.2.2",
|
||||
"semver": "^6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -170,9 +170,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.7.tgz",
|
||||
"integrity": "sha512-LtoWbDXOaidEf50hmdDqn9g8VEzsorMexoWMQdQODbvmqYmaF23pBP5VNPAGIFHsFQCIeKokDiz3CH5Y2jlY6w==",
|
||||
"version": "7.20.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz",
|
||||
"integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.18.6",
|
||||
@@ -181,6 +181,7 @@
|
||||
"@babel/helper-member-expression-to-functions": "^7.20.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.18.6",
|
||||
"@babel/helper-replace-supers": "^7.20.7",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1698,9 +1699,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.20.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.10.tgz",
|
||||
"integrity": "sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg==",
|
||||
"version": "7.20.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz",
|
||||
"integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
@@ -3018,9 +3019,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
|
||||
"integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
@@ -3096,9 +3097,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-loader": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.0.tgz",
|
||||
"integrity": "sha512-Antt61KJPinUMwHwIIz9T5zfMgevnfZkEVWYDWlG888fgdvRRGD0JTuf/fFozQnfT+uq64sk1bmdHDy/mOEWnA==",
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz",
|
||||
"integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"find-cache-dir": "^3.3.2",
|
||||
@@ -3301,9 +3302,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001441",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz",
|
||||
"integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==",
|
||||
"version": "1.0.30001442",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz",
|
||||
"integrity": "sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3935,9 +3936,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz",
|
||||
"integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -4869,9 +4870,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.20",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
|
||||
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
|
||||
"version": "8.4.21",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
|
||||
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -6392,9 +6393,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-cli/node_modules/commander": {
|
||||
"version": "9.4.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz",
|
||||
"integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==",
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
@@ -6650,25 +6651,25 @@
|
||||
"dev": true
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.7.tgz",
|
||||
"integrity": "sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw==",
|
||||
"version": "7.20.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz",
|
||||
"integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@ampproject/remapping": "^2.1.0",
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^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/parser": "^7.20.7",
|
||||
"@babel/template": "^7.20.7",
|
||||
"@babel/traverse": "^7.20.7",
|
||||
"@babel/traverse": "^7.20.12",
|
||||
"@babel/types": "^7.20.7",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
"json5": "^2.2.1",
|
||||
"json5": "^2.2.2",
|
||||
"semver": "^6.3.0"
|
||||
}
|
||||
},
|
||||
@@ -6729,9 +6730,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.7.tgz",
|
||||
"integrity": "sha512-LtoWbDXOaidEf50hmdDqn9g8VEzsorMexoWMQdQODbvmqYmaF23pBP5VNPAGIFHsFQCIeKokDiz3CH5Y2jlY6w==",
|
||||
"version": "7.20.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz",
|
||||
"integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.18.6",
|
||||
@@ -6740,6 +6741,7 @@
|
||||
"@babel/helper-member-expression-to-functions": "^7.20.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.18.6",
|
||||
"@babel/helper-replace-supers": "^7.20.7",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6"
|
||||
}
|
||||
},
|
||||
@@ -7771,9 +7773,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.20.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.10.tgz",
|
||||
"integrity": "sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg==",
|
||||
"version": "7.20.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz",
|
||||
"integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
@@ -8889,9 +8891,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
|
||||
"integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
@@ -8943,9 +8945,9 @@
|
||||
}
|
||||
},
|
||||
"babel-loader": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.0.tgz",
|
||||
"integrity": "sha512-Antt61KJPinUMwHwIIz9T5zfMgevnfZkEVWYDWlG888fgdvRRGD0JTuf/fFozQnfT+uq64sk1bmdHDy/mOEWnA==",
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz",
|
||||
"integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"find-cache-dir": "^3.3.2",
|
||||
@@ -9082,9 +9084,9 @@
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001441",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz",
|
||||
"integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==",
|
||||
"version": "1.0.30001442",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz",
|
||||
"integrity": "sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
@@ -9545,9 +9547,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"fastq": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz",
|
||||
"integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -10234,9 +10236,9 @@
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.20",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
|
||||
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
|
||||
"version": "8.4.21",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
|
||||
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
@@ -11283,9 +11285,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "9.4.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz",
|
||||
"integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==",
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "tinode-webapp",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.21.0-beta1",
|
||||
"version": "0.21.0-rc1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinode/example-react-js.git"
|
||||
},
|
||||
"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",
|
||||
"clean": "rm umd/* && rm css/base.min.css && rm i18n.min/*",
|
||||
"build:css": "postcss css/base.css > css/base.min.css",
|
||||
@@ -67,7 +68,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-intl": "^6.2.1",
|
||||
"tinode-sdk": "^0.21.0-beta1",
|
||||
"tinode-sdk": "^0.21.0-rc1",
|
||||
"webm-duration-fix": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+31
-31
@@ -64,16 +64,16 @@ const fbMessaging = firebase.messaging();
|
||||
|
||||
// This method shows the push notifications while the window is in background.
|
||||
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.
|
||||
if (webAppChannel) {
|
||||
webAppChannel.postMessage(payload.data);
|
||||
}
|
||||
|
||||
if (payload.data.silent == 'true') {
|
||||
// No need to show anything.
|
||||
return;
|
||||
}
|
||||
|
||||
const titles = {'msg': 'new_message', 'sub': 'new_chat'};
|
||||
const pushType = payload.data.what || 'msg';
|
||||
const title = payload.data.title || self.i18nMessage(titles[pushType]);
|
||||
@@ -111,35 +111,35 @@ self.addEventListener('notificationclick', event => {
|
||||
|
||||
event.waitUntil(self.clients.matchAll({
|
||||
type: 'window',
|
||||
includeUncontrolled: true
|
||||
}).then(windowClients => {
|
||||
let anyClient = null;
|
||||
for (let i = 0; i < windowClients.length; i++) {
|
||||
const url = new URL(windowClients[i].url);
|
||||
if (url.hash.includes(data.topic)) {
|
||||
// Found the Tinode tab with the right topic open.
|
||||
return windowClients[i].focus();
|
||||
} else {
|
||||
// This will be the least recently used tab.
|
||||
anyClient = windowClients[i];
|
||||
includeUncontrolled: true})
|
||||
.then(windowClients => {
|
||||
let anyClient = null;
|
||||
for (let i = 0; i < windowClients.length; i++) {
|
||||
const url = new URL(windowClients[i].url);
|
||||
if (url.hash.includes(data.topic)) {
|
||||
// Found the Tinode tab with the right topic open.
|
||||
return windowClients[i].focus();
|
||||
} else {
|
||||
// This will be the least recently used tab.
|
||||
anyClient = windowClients[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Found tab with Tinode on a different topic,
|
||||
// navigate to the right topic.
|
||||
if (anyClient) {
|
||||
const url = new URL(anyClient.url);
|
||||
// Found tab with Tinode on a different topic,
|
||||
// navigate to the right topic.
|
||||
if (anyClient) {
|
||||
const url = new URL(anyClient.url);
|
||||
url.hash = urlHash;
|
||||
return anyClient.focus().then(thisClient => {
|
||||
return thisClient.navigate(url);
|
||||
});
|
||||
}
|
||||
|
||||
// Did not find a Tinode browser tab. Open one.
|
||||
const url = new URL(self.location.origin);
|
||||
url.hash = urlHash;
|
||||
return anyClient.focus().then(thisClient => {
|
||||
return thisClient.navigate(url);
|
||||
});
|
||||
}
|
||||
|
||||
// Did not find a Tinode browser tab. Open one.
|
||||
const url = new URL(self.location.origin);
|
||||
url.hash = urlHash;
|
||||
return clients.openWindow(url);
|
||||
}));
|
||||
return clients.openWindow(url);
|
||||
}));
|
||||
});
|
||||
|
||||
// This is needed for 'Add to Home Screen'.
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@ export const CALL_STATE_OUTGOING_INITATED = 1;
|
||||
export const CALL_STATE_INCOMING_RECEIVED = 2;
|
||||
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';
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1543,6 +1543,13 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Das Format dieses Videos wird nicht erkannt",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
|
||||
@@ -1543,6 +1543,13 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "Call",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Format of this video is not recognized",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
|
||||
@@ -1543,6 +1543,13 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "No se reconoce el formato de este video",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
|
||||
@@ -500,6 +500,10 @@
|
||||
"defaultMessage": "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": {
|
||||
"defaultMessage": "Block",
|
||||
"description": "Block topic or user"
|
||||
|
||||
@@ -1543,6 +1543,13 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Le format de cette vidéo n'est pas reconnu",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
|
||||
@@ -1543,6 +1543,13 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "이 동영상의 형식을 인식할 수 없습니다.",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
|
||||
@@ -1543,6 +1543,13 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Formatul acestui videoclip nu este recunoscut",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
|
||||
@@ -1543,6 +1543,13 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Неизвестный формат видео файла",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
|
||||
@@ -1543,6 +1543,13 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "無法識別此視頻的格式",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
|
||||
@@ -1543,6 +1543,13 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "无法识别此视频的格式",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
export const PACKAGE_VERSION = "0.21.0-beta1";
|
||||
export const PACKAGE_VERSION = "0.21.0-rc1";
|
||||
|
||||
@@ -303,8 +303,6 @@ class MessagesView extends React.Component {
|
||||
scrollPosition: 0,
|
||||
fetchingMessages: false,
|
||||
showGoToLastButton: false,
|
||||
forwardMessage: null,
|
||||
reply: null,
|
||||
contentToEdit: null,
|
||||
dragging: false
|
||||
};
|
||||
@@ -315,6 +313,8 @@ class MessagesView extends React.Component {
|
||||
content: nextProps.forwardMessage.preview,
|
||||
seq: null
|
||||
};
|
||||
} else {
|
||||
nextState.reply = null;
|
||||
}
|
||||
|
||||
if (topic) {
|
||||
@@ -1358,6 +1358,7 @@ class MessagesView extends React.Component {
|
||||
topic={this.state.topic}
|
||||
seq={this.props.callSeq}
|
||||
callState={this.props.callState}
|
||||
callAudioOnly={this.props.callAudioOnly}
|
||||
tinode={this.props.tinode}
|
||||
title={this.state.title}
|
||||
avatar={this.state.avatar || true}
|
||||
|
||||
+71
-37
@@ -69,6 +69,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Info',
|
||||
description: 'Show extended topic information'
|
||||
},
|
||||
menu_item_audio_call: {
|
||||
id: 'menu_item_audio_call',
|
||||
defaultMessage: 'Call',
|
||||
description: 'Start audio call'
|
||||
},
|
||||
menu_item_video_call: {
|
||||
id: 'menu_item_video_call',
|
||||
defaultMessage: 'Video call',
|
||||
@@ -158,6 +163,7 @@ class TinodeWeb extends React.Component {
|
||||
this.handleHideForwardDialog = this.handleHideForwardDialog.bind(this);
|
||||
|
||||
this.handleStartVideoCall = this.handleStartVideoCall.bind(this);
|
||||
this.handleStartAudioCall = this.handleStartAudioCall.bind(this);
|
||||
this.handleInfoMessage = this.handleInfoMessage.bind(this);
|
||||
this.handleDataMessage = this.handleDataMessage.bind(this);
|
||||
this.handleCallClose = this.handleCallClose.bind(this);
|
||||
@@ -233,6 +239,7 @@ class TinodeWeb extends React.Component {
|
||||
// Video calls.
|
||||
callTopic: undefined,
|
||||
callState: CALL_STATE_NONE,
|
||||
callAudioOnly: undefined,
|
||||
// If true, call state should be transitioned to CALL_STATE_IN_PROGRESS upon
|
||||
// switching to the call topic.
|
||||
callShouldStart: false,
|
||||
@@ -299,11 +306,7 @@ class TinodeWeb extends React.Component {
|
||||
}).then(_ => {
|
||||
// Initialize desktop alerts.
|
||||
if (this.state.desktopAlertsEnabled) {
|
||||
this.initFCMessaging().then(_ => {
|
||||
if (this.state.desktopAlerts) {
|
||||
this.tinode.setDeviceToken(this.state.firebaseToken);
|
||||
}
|
||||
}).catch(_ => {
|
||||
this.initFCMessaging().catch(_ => {
|
||||
// do nothing: handled earlier.
|
||||
// catch needed to pervent unnecessary logging of error.
|
||||
});
|
||||
@@ -370,20 +373,23 @@ class TinodeWeb extends React.Component {
|
||||
const onError = (msg, err) => {
|
||||
console.error(msg, err);
|
||||
this.handleError(formatMessage(messages.push_init_failed), 'err');
|
||||
this.setState({desktopAlertsEnabled: false, firebaseToken: null});
|
||||
this.setState({firebaseToken: null});
|
||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
||||
}
|
||||
|
||||
try {
|
||||
this.fcm = firebaseGetMessaging(firebaseInitApp(FIREBASE_INIT, APP_NAME));
|
||||
return navigator.serviceWorker.register('/service-worker.js').then((reg) => {
|
||||
this.checkForAppUpdate(reg);
|
||||
reg.active.postMessage(JSON.stringify({locale: locale, version: PACKAGE_VERSION}));
|
||||
return reg;
|
||||
}).then((reg) => {
|
||||
return navigator.serviceWorker.getRegistration('/service-worker.js').then(reg => {
|
||||
return reg || navigator.serviceWorker.register('/service-worker.js').then(reg => {
|
||||
this.checkForAppUpdate(reg);
|
||||
return reg;
|
||||
});
|
||||
}).then(reg => {
|
||||
// Pass locale and version config to the service worker.
|
||||
(reg.active || reg.installing).postMessage(JSON.stringify({locale: locale, version: PACKAGE_VERSION}));
|
||||
// Request token.
|
||||
return TinodeWeb.requestFCMToken(this.fcm, reg);
|
||||
}).then((token) => {
|
||||
}).then(token => {
|
||||
const persist = LocalStorageUtil.getObject('keep-logged-in');
|
||||
if (token != this.state.firebaseToken) {
|
||||
this.tinode.setDeviceToken(token);
|
||||
@@ -399,7 +405,7 @@ class TinodeWeb extends React.Component {
|
||||
// Handhe FCM pushes
|
||||
// (a) for channels always,
|
||||
// (b) pushes when the app is in foreground but has no focus.
|
||||
firebaseOnMessage(this.fcm, (payload) => { this.handlePushMessage(payload); });
|
||||
firebaseOnMessage(this.fcm, payload => { this.handlePushMessage(payload); });
|
||||
}).catch(err => {
|
||||
// SW registration or FCM has failed :(
|
||||
onError(err);
|
||||
@@ -416,17 +422,17 @@ class TinodeWeb extends React.Component {
|
||||
return firebaseGetToken(fcm, {
|
||||
serviceWorkerRegistration: sw,
|
||||
vapidKey: FIREBASE_INIT.messagingVapidKey
|
||||
}).then((token) => {
|
||||
}).then(token => {
|
||||
if (token) {
|
||||
return token;
|
||||
} else {
|
||||
// Try to request permissions.
|
||||
return Notification.requestPermission().then((permission) => {
|
||||
return Notification.requestPermission().then(permission => {
|
||||
if (permission === 'granted') {
|
||||
return firebaseGetToken(fcm, {
|
||||
serviceWorkerRegistration: reg,
|
||||
vapidKey: FIREBASE_INIT.messagingVapidKey
|
||||
}).then((token) => {
|
||||
}).then(token => {
|
||||
if (token) {
|
||||
return token;
|
||||
} else {
|
||||
@@ -576,6 +582,12 @@ class TinodeWeb extends React.Component {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.desktopAlertsEnabled && !this.state.firebaseToken) {
|
||||
// Firefox and Safari: "The Notification permission may only be requested from inside a
|
||||
// short running user-generated event handler".
|
||||
this.initFCMessaging();
|
||||
}
|
||||
}
|
||||
|
||||
// Enable or disable saving the password and IndexedDB.
|
||||
@@ -712,20 +724,23 @@ class TinodeWeb extends React.Component {
|
||||
this.handleLoginSuccessful();
|
||||
}
|
||||
}).catch(err => {
|
||||
const autoLogin = err.code >= 500;
|
||||
// Connection or login failed, report error.
|
||||
this.setState({
|
||||
loginDisabled: false,
|
||||
credMethod: undefined,
|
||||
credCode: undefined,
|
||||
loadSpinnerVisible: false,
|
||||
autoLogin: false
|
||||
autoLogin: autoLogin
|
||||
});
|
||||
this.handleError(err.message, 'err');
|
||||
console.warn("Login failed", err);
|
||||
if (token) {
|
||||
this.handleLogout();
|
||||
if (!autoLogin) {
|
||||
if (token) {
|
||||
this.handleLogout();
|
||||
}
|
||||
HashNavigation.navigateTo('');
|
||||
}
|
||||
HashNavigation.navigateTo('');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1584,6 +1599,10 @@ class TinodeWeb extends React.Component {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_info),
|
||||
handler: this.handleShowInfoView
|
||||
} : 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 ? {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_video_call),
|
||||
handler: this.handleStartVideoCall
|
||||
@@ -1709,15 +1728,24 @@ class TinodeWeb extends React.Component {
|
||||
handleStartVideoCall() {
|
||||
this.setState({
|
||||
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) {
|
||||
case CALL_STATE_OUTGOING_INITATED:
|
||||
let head = { webrtc: CALL_HEAD_STARTED };
|
||||
this.handleSendMessage(Drafty.videoCall(), undefined, undefined, head)
|
||||
const head = { webrtc: CALL_HEAD_STARTED, aonly: !!audioOnly };
|
||||
this.handleSendMessage(Drafty.videoCall(audioOnly), undefined, undefined, head)
|
||||
.then(ctrl => {
|
||||
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
|
||||
this.handleCallClose();
|
||||
@@ -1787,7 +1815,8 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
this.setState({
|
||||
callTopic: undefined,
|
||||
callState: CALL_STATE_NONE
|
||||
callState: CALL_STATE_NONE,
|
||||
callAudioOnly: undefined
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1820,7 +1849,8 @@ class TinodeWeb extends React.Component {
|
||||
this.setState({
|
||||
callTopic: null,
|
||||
callState: CALL_STATE_NONE,
|
||||
callSeq: null
|
||||
callSeq: null,
|
||||
callAudioOnly: undefined
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1853,7 +1883,8 @@ class TinodeWeb extends React.Component {
|
||||
this.setState({
|
||||
callTopic: data.topic,
|
||||
callState: CALL_STATE_INCOMING_RECEIVED,
|
||||
callSeq: data.seq
|
||||
callSeq: data.seq,
|
||||
callAudioOnly: !!msg.head.aonly
|
||||
});
|
||||
} else {
|
||||
// Another call is either in progress or being established.
|
||||
@@ -1909,10 +1940,11 @@ class TinodeWeb extends React.Component {
|
||||
{this.state.callTopic && this.state.callState == CALL_STATE_INCOMING_RECEIVED ?
|
||||
<CallIncoming
|
||||
tinode={this.tinode}
|
||||
onClose={this.handleCallClose}
|
||||
topic={this.state.callTopic}
|
||||
seq={this.state.callSeq}
|
||||
callState={this.state.callState}
|
||||
audioOnly={this.state.callAudioOnly}
|
||||
onClose={this.handleCallClose}
|
||||
onRinging={this.handleCallRinging}
|
||||
onAcceptCall={this.handleCallAccept}
|
||||
onReject={this.handleCallHangup}
|
||||
@@ -1920,15 +1952,16 @@ class TinodeWeb extends React.Component {
|
||||
:
|
||||
null
|
||||
}
|
||||
<Alert
|
||||
visible={this.state.alertVisible}
|
||||
title={this.state.alertParams.title}
|
||||
content={this.state.alertParams.content}
|
||||
onReject={this.state.alertParams.onReject ? (_ => this.setState({alertVisible: false})) : null}
|
||||
reject={this.state.alertParams.reject}
|
||||
onConfirm={_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}}
|
||||
confirm={this.state.alertParams.confirm}
|
||||
/>
|
||||
{this.state.alertVisible ?
|
||||
<Alert
|
||||
visible={this.state.alertVisible}
|
||||
title={this.state.alertParams.title}
|
||||
content={this.state.alertParams.content}
|
||||
onReject={this.state.alertParams.onReject ? (_ => this.setState({alertVisible: false})) : null}
|
||||
reject={this.state.alertParams.reject}
|
||||
onConfirm={_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}}
|
||||
confirm={this.state.alertParams.confirm}
|
||||
/> : null}
|
||||
<SidepanelView
|
||||
tinode={this.tinode}
|
||||
connected={this.state.connected}
|
||||
@@ -2024,6 +2057,7 @@ class TinodeWeb extends React.Component {
|
||||
callTopic={this.state.callTopic}
|
||||
callSeq={this.state.callSeq}
|
||||
callState={this.state.callState}
|
||||
callAudioOnly={this.state.callAudioOnly}
|
||||
onCallHangup={this.handleCallHangup}
|
||||
|
||||
onCallInvite={this.handleCallInvite}
|
||||
|
||||
+15
-18
@@ -3,26 +3,23 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class Alert extends React.PureComponent {
|
||||
render() {
|
||||
return this.props.visible ?
|
||||
<div className="alert-container">
|
||||
<div className="alert">
|
||||
<div className="title">{this.props.title}</div>
|
||||
<div className="content">{this.props.content}</div>
|
||||
<div className="dialog-buttons">
|
||||
{this.props.onReject ?
|
||||
<button className="outline" onClick={this.props.onReject}>
|
||||
{this.props.reject || <FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />}
|
||||
</button>
|
||||
:
|
||||
null
|
||||
}
|
||||
<button className="primary" onClick={this.props.onConfirm}>
|
||||
{this.props.confirm || <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />}
|
||||
<div className="alert-container">
|
||||
<div className="alert">
|
||||
<div className="title">{this.props.title}</div>
|
||||
<div className="content">{this.props.content}</div>
|
||||
<div className="dialog-buttons">
|
||||
{this.props.onReject ?
|
||||
<button className="outline" onClick={this.props.onReject}>
|
||||
{this.props.reject || <FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />}
|
||||
</button>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<button className="primary" onClick={this.props.onConfirm}>
|
||||
{this.props.confirm || <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null;
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
+206
-39
@@ -15,6 +15,9 @@ const CALL_ENDED_SOUND = new Audio('audio/call-end.m4a');
|
||||
CALL_ENDED_SOUND.loop = true;
|
||||
const DIALING_SOUND = new Audio('audio/dialing.m4a');
|
||||
|
||||
const VIDEO_MUTED_EVENT = 'video:muted';
|
||||
const VIDEO_UNMUTED_EVENT = 'video:unmuted';
|
||||
|
||||
const messages = defineMessages({
|
||||
already_in_call: {
|
||||
id: 'already_in_call',
|
||||
@@ -30,16 +33,22 @@ class CallPanel extends React.PureComponent {
|
||||
this.state = {
|
||||
localStream: undefined,
|
||||
pc: undefined,
|
||||
dataChannel: undefined,
|
||||
|
||||
previousOnInfo: undefined,
|
||||
waitingForPeer: false,
|
||||
// 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 = {
|
||||
audio: true,
|
||||
video: true
|
||||
video: !props.callAudioOnly
|
||||
};
|
||||
this.isOutgoingCall = props.callState == CALL_STATE_OUTGOING_INITATED;
|
||||
|
||||
@@ -77,10 +86,19 @@ class CallPanel extends React.PureComponent {
|
||||
this.handleCloseClick = this.handleCloseClick.bind(this);
|
||||
this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
|
||||
this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
|
||||
this.toggleMedia = this.toggleMedia.bind(this);
|
||||
|
||||
this.handleRemoteHangup = this.handleRemoteHangup.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() {
|
||||
@@ -105,6 +123,13 @@ class CallPanel extends React.PureComponent {
|
||||
const stream = this.state.localStream;
|
||||
stream.getTracks().forEach(track => {
|
||||
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() {
|
||||
if (this.state.localStream) {
|
||||
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) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -154,13 +189,18 @@ class CallPanel extends React.PureComponent {
|
||||
// Start local video.
|
||||
navigator.mediaDevices.getUserMedia(this.localStreamConstraints)
|
||||
.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.localRef.current.srcObject = stream;
|
||||
|
||||
DIALING_SOUND.play();
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -183,7 +223,11 @@ class CallPanel extends React.PureComponent {
|
||||
this.state.pc.onicegatheringstatechange = null;
|
||||
this.state.pc.onnegotiationneeded = null;
|
||||
this.state.pc.onicecandidateerror = null;
|
||||
this.state.pc.ondatachannel = null;
|
||||
|
||||
if (this.state.dataChannel) {
|
||||
this.state.dataChannel.close();
|
||||
}
|
||||
this.state.pc.close();
|
||||
}
|
||||
this.setState({pc: null, waitingForPeer: false});
|
||||
@@ -209,6 +253,42 @@ class CallPanel extends React.PureComponent {
|
||||
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() {
|
||||
const iceServers = this.props.tinode.getServerParam('iceServers', null);
|
||||
const pc = iceServers ? new RTCPeerConnection({iceServers: iceServers}) : new RTCPeerConnection();
|
||||
@@ -220,8 +300,17 @@ class CallPanel extends React.PureComponent {
|
||||
pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
|
||||
pc.onicecandidateerror = this.handleIceCandidateErrorEvent;
|
||||
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});
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
@@ -233,7 +322,7 @@ class CallPanel extends React.PureComponent {
|
||||
.then(_ => {
|
||||
this.setState({ callInitialSetupComplete: true }, _ => this.drainRemoteIceCandidatesCache());
|
||||
})
|
||||
.catch(this.reportError);
|
||||
.catch(err => this.reportError(err));
|
||||
}
|
||||
|
||||
reportError(err) {
|
||||
@@ -244,17 +333,18 @@ class CallPanel extends React.PureComponent {
|
||||
return this.isOutgoingCall || this.state.callInitialSetupComplete;
|
||||
}
|
||||
|
||||
handleNegotiationNeededEvent() {
|
||||
handleNegotiationNeededEvent(event) {
|
||||
const pc = event.target;
|
||||
if (!this.canSendOffer()) {
|
||||
return;
|
||||
}
|
||||
this.state.pc.createOffer().then(offer => {
|
||||
return this.state.pc.setLocalDescription(offer);
|
||||
pc.createOffer().then(offer => {
|
||||
return pc.setLocalDescription(offer);
|
||||
})
|
||||
.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) {
|
||||
@@ -271,7 +361,12 @@ class CallPanel extends React.PureComponent {
|
||||
const candidate = new RTCIceCandidate(info.payload);
|
||||
if (this.state.callInitialSetupComplete) {
|
||||
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 {
|
||||
this.remoteIceCandidatesCache.push(candidate);
|
||||
}
|
||||
@@ -280,7 +375,12 @@ class CallPanel extends React.PureComponent {
|
||||
drainRemoteIceCandidatesCache() {
|
||||
this.remoteIceCandidatesCache.forEach(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 = [];
|
||||
}
|
||||
@@ -295,7 +395,7 @@ class CallPanel extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleSignalingStateChangeEvent(event) {
|
||||
if (this.state.pc.signalingState == 'closed') {
|
||||
if (event.target.signalingState == 'closed') {
|
||||
this.handleCloseClick();
|
||||
}
|
||||
}
|
||||
@@ -307,23 +407,24 @@ class CallPanel extends React.PureComponent {
|
||||
handleTrackEvent(event) {
|
||||
// Remote video becomes available.
|
||||
this.remoteRef.current.srcObject = event.streams[0];
|
||||
|
||||
// Make sure we display the title (peer's name) over the remote video.
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
handleGetUserMediaError(e) {
|
||||
console.error("Error opening camera and/or microphone", e);
|
||||
switch(e.name) {
|
||||
case 'NotFoundError':
|
||||
// Cannot start the call b/c no camera and/or microphone found.
|
||||
this.reportError(e.message);
|
||||
this.reportError(e);
|
||||
break;
|
||||
case 'SecurityError':
|
||||
case 'PermissionDeniedError':
|
||||
// Do nothing; this is the same as the user canceling the call.
|
||||
break;
|
||||
default:
|
||||
this.reportError(e.message);
|
||||
console.error("Error opening your camera and/or microphone:", e.message);
|
||||
this.reportError(e);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -334,13 +435,20 @@ class CallPanel extends React.PureComponent {
|
||||
|
||||
handleVideoOfferMsg(info) {
|
||||
let localStream = null;
|
||||
const pc = this.createPeerConnection();
|
||||
const pc = this.state.pc ? this.state.pc : this.createPeerConnection();
|
||||
const desc = new RTCSessionDescription(info.payload);
|
||||
|
||||
pc.setRemoteDescription(desc).then(_ => {
|
||||
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
|
||||
})
|
||||
.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;
|
||||
this.localRef.current.srcObject = stream;
|
||||
this.setState({localStream: stream});
|
||||
@@ -348,6 +456,11 @@ class CallPanel extends React.PureComponent {
|
||||
localStream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, localStream);
|
||||
});
|
||||
|
||||
if (dummyVideo) {
|
||||
dummyVideo.stop();
|
||||
stream.removeTrack(dummyVideo);
|
||||
}
|
||||
})
|
||||
.then(_ => {
|
||||
return pc.createAnswer();
|
||||
@@ -387,49 +500,103 @@ class CallPanel extends React.PureComponent {
|
||||
this.props.onHangup(this.props.topic, this.props.seq);
|
||||
}
|
||||
|
||||
// Mute/unmute audio/video (specified by kind).
|
||||
toggleMedia(kind) {
|
||||
// Ends video track and turns off the camera.
|
||||
muteVideo() {
|
||||
if (!this.state.pc || !this.state.dataChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = this.state.localStream;
|
||||
stream.getTracks().forEach(track => {
|
||||
if (track.kind != kind) {
|
||||
return;
|
||||
const t = stream.getVideoTracks()[0];
|
||||
t.enabled = false;
|
||||
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).
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
handleToggleCameraClick() {
|
||||
this.toggleMedia('video');
|
||||
}
|
||||
|
||||
handleToggleMicClick() {
|
||||
this.toggleMedia('audio');
|
||||
}
|
||||
|
||||
render() {
|
||||
const remoteActive = this.remoteRef.current && this.remoteRef.current.srcObject;
|
||||
const disabled = !(this.state.localStream && this.state.localStream.getTracks());
|
||||
const audioIcon = this.state.localStream && this.state.localStream.getAudioTracks()[0].enabled ? 'mic' : 'mic_off';
|
||||
const videoIcon = this.state.localStream && this.state.localStream.getVideoTracks()[0].enabled ? 'videocam' : 'videocam_off';
|
||||
const audioTracks = this.state.localStream && this.state.localStream.getAudioTracks();
|
||||
const videoTracks = !this.state.audioOnly && this.state.localStream && this.state.localStream.getVideoTracks();
|
||||
const disabled = !this.state.pc || !this.state.dataChannel || !(audioTracks && audioTracks[0]);
|
||||
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 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 (
|
||||
<>
|
||||
<div id="video-container">
|
||||
<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 />
|
||||
<div className="caller-name inactive">
|
||||
<FormattedMessage id="calls_you_label"
|
||||
defaultMessage="You" description="Shown over the local video screen" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="call-party peer">
|
||||
<div className="call-party peer" disabled={!removeActive}>
|
||||
<video ref={this.remoteRef} autoPlay playsInline />
|
||||
{remoteActive ?
|
||||
{removeActive ?
|
||||
<div className="caller-name inactive">{peerTitle}</div> :
|
||||
<div className={`caller-card${pulseAnimation}`}>
|
||||
<div className="avatar-box">
|
||||
|
||||
@@ -28,7 +28,7 @@ export default class CallStatus extends React.PureComponent {
|
||||
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
|
||||
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" />
|
||||
} else {
|
||||
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+251
-66
@@ -3544,7 +3544,7 @@ __webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ "PACKAGE_VERSION": () => (/* binding */ PACKAGE_VERSION)
|
||||
/* harmony export */ });
|
||||
const PACKAGE_VERSION = "0.21.0-beta1";
|
||||
const PACKAGE_VERSION = "0.21.0-rc1";
|
||||
|
||||
/***/ }),
|
||||
|
||||
@@ -5721,8 +5721,6 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
scrollPosition: 0,
|
||||
fetchingMessages: false,
|
||||
showGoToLastButton: false,
|
||||
forwardMessage: null,
|
||||
reply: null,
|
||||
contentToEdit: null,
|
||||
dragging: false
|
||||
};
|
||||
@@ -5731,6 +5729,8 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
content: nextProps.forwardMessage.preview,
|
||||
seq: null
|
||||
};
|
||||
} else {
|
||||
nextState.reply = null;
|
||||
}
|
||||
if (topic) {
|
||||
const subs = [];
|
||||
@@ -6635,6 +6635,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
topic: this.state.topic,
|
||||
seq: this.props.callSeq,
|
||||
callState: this.props.callState,
|
||||
callAudioOnly: this.props.callAudioOnly,
|
||||
tinode: this.props.tinode,
|
||||
title: this.state.title,
|
||||
avatar: this.state.avatar || true,
|
||||
@@ -7772,6 +7773,13 @@ const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
|
||||
"value": "Info"
|
||||
}]
|
||||
},
|
||||
menu_item_audio_call: {
|
||||
id: "menu_item_audio_call",
|
||||
defaultMessage: [{
|
||||
"type": 0,
|
||||
"value": "Call"
|
||||
}]
|
||||
},
|
||||
menu_item_video_call: {
|
||||
id: "menu_item_video_call",
|
||||
defaultMessage: [{
|
||||
@@ -7859,6 +7867,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
this.handleShowForwardDialog = this.handleShowForwardDialog.bind(this);
|
||||
this.handleHideForwardDialog = this.handleHideForwardDialog.bind(this);
|
||||
this.handleStartVideoCall = this.handleStartVideoCall.bind(this);
|
||||
this.handleStartAudioCall = this.handleStartAudioCall.bind(this);
|
||||
this.handleInfoMessage = this.handleInfoMessage.bind(this);
|
||||
this.handleDataMessage = this.handleDataMessage.bind(this);
|
||||
this.handleCallClose = this.handleCallClose.bind(this);
|
||||
@@ -7913,6 +7922,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
mobilePanel: 'sidepanel',
|
||||
callTopic: undefined,
|
||||
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE,
|
||||
callAudioOnly: undefined,
|
||||
callShouldStart: false,
|
||||
contextMenuVisible: false,
|
||||
contextMenuBounds: null,
|
||||
@@ -7961,11 +7971,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
this.tinode.onDataMessage = this.handleDataMessage;
|
||||
}).then(_ => {
|
||||
if (this.state.desktopAlertsEnabled) {
|
||||
this.initFCMessaging().then(_ => {
|
||||
if (this.state.desktopAlerts) {
|
||||
this.tinode.setDeviceToken(this.state.firebaseToken);
|
||||
}
|
||||
}).catch(_ => {});
|
||||
this.initFCMessaging().catch(_ => {});
|
||||
}
|
||||
const parsedNav = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_18__["default"].parseUrlHash(window.location.hash);
|
||||
this.resetContactList();
|
||||
@@ -8023,7 +8029,6 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
console.error(msg, err);
|
||||
this.handleError(formatMessage(messages.push_init_failed), 'err');
|
||||
this.setState({
|
||||
desktopAlertsEnabled: false,
|
||||
firebaseToken: null
|
||||
});
|
||||
_lib_local_storage_js__WEBPACK_IMPORTED_MODULE_17__["default"].updateObject('settings', {
|
||||
@@ -8032,14 +8037,16 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
};
|
||||
try {
|
||||
this.fcm = (0,firebase_messaging__WEBPACK_IMPORTED_MODULE_3__.getMessaging)((0,firebase_app__WEBPACK_IMPORTED_MODULE_2__.initializeApp)(FIREBASE_INIT, _config_js__WEBPACK_IMPORTED_MODULE_12__.APP_NAME));
|
||||
return navigator.serviceWorker.register('/service-worker.js').then(reg => {
|
||||
this.checkForAppUpdate(reg);
|
||||
reg.active.postMessage(JSON.stringify({
|
||||
return navigator.serviceWorker.getRegistration('/service-worker.js').then(reg => {
|
||||
return reg || navigator.serviceWorker.register('/service-worker.js').then(reg => {
|
||||
this.checkForAppUpdate(reg);
|
||||
return reg;
|
||||
});
|
||||
}).then(reg => {
|
||||
(reg.active || reg.installing).postMessage(JSON.stringify({
|
||||
locale: locale,
|
||||
version: _version_js__WEBPACK_IMPORTED_MODULE_14__.PACKAGE_VERSION
|
||||
}));
|
||||
return reg;
|
||||
}).then(reg => {
|
||||
return TinodeWeb.requestFCMToken(this.fcm, reg);
|
||||
}).then(token => {
|
||||
const persist = _lib_local_storage_js__WEBPACK_IMPORTED_MODULE_17__["default"].getObject('keep-logged-in');
|
||||
@@ -8229,6 +8236,9 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
if (this.state.desktopAlertsEnabled && !this.state.firebaseToken) {
|
||||
this.initFCMessaging();
|
||||
}
|
||||
}
|
||||
handlePersistenceChange(persist) {
|
||||
if (persist) {
|
||||
@@ -8347,19 +8357,22 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
this.handleLoginSuccessful();
|
||||
}
|
||||
}).catch(err => {
|
||||
const autoLogin = err.code >= 500;
|
||||
this.setState({
|
||||
loginDisabled: false,
|
||||
credMethod: undefined,
|
||||
credCode: undefined,
|
||||
loadSpinnerVisible: false,
|
||||
autoLogin: false
|
||||
autoLogin: autoLogin
|
||||
});
|
||||
this.handleError(err.message, 'err');
|
||||
console.warn("Login failed", err);
|
||||
if (token) {
|
||||
this.handleLogout();
|
||||
if (!autoLogin) {
|
||||
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) {
|
||||
@@ -9105,6 +9118,9 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
return [subscribed ? {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_info),
|
||||
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 ? {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_video_call),
|
||||
handler: this.handleStartVideoCall
|
||||
@@ -9213,16 +9229,25 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
handleStartVideoCall() {
|
||||
this.setState({
|
||||
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) {
|
||||
case _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED:
|
||||
let head = {
|
||||
webrtc: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_HEAD_STARTED
|
||||
const head = {
|
||||
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) {
|
||||
this.handleCallClose();
|
||||
return;
|
||||
@@ -9282,7 +9307,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
}
|
||||
this.setState({
|
||||
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) {
|
||||
@@ -9311,7 +9337,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
this.setState({
|
||||
callTopic: null,
|
||||
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE,
|
||||
callSeq: null
|
||||
callSeq: null,
|
||||
callAudioOnly: undefined
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -9337,7 +9364,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
this.setState({
|
||||
callTopic: data.topic,
|
||||
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_INCOMING_RECEIVED,
|
||||
callSeq: data.seq
|
||||
callSeq: data.seq,
|
||||
callAudioOnly: !!msg.head.aonly
|
||||
});
|
||||
} else {
|
||||
this.handleCallHangup(data.topic, data.seq);
|
||||
@@ -9380,14 +9408,15 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
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"], {
|
||||
tinode: this.tinode,
|
||||
onClose: this.handleCallClose,
|
||||
topic: this.state.callTopic,
|
||||
seq: this.state.callSeq,
|
||||
callState: this.state.callState,
|
||||
audioOnly: this.state.callAudioOnly,
|
||||
onClose: this.handleCallClose,
|
||||
onRinging: this.handleCallRinging,
|
||||
onAcceptCall: this.handleCallAccept,
|
||||
onReject: this.handleCallHangup
|
||||
}) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_alert_jsx__WEBPACK_IMPORTED_MODULE_5__["default"], {
|
||||
}) : null, this.state.alertVisible ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_alert_jsx__WEBPACK_IMPORTED_MODULE_5__["default"], {
|
||||
visible: this.state.alertVisible,
|
||||
title: this.state.alertParams.title,
|
||||
content: this.state.alertParams.content,
|
||||
@@ -9402,7 +9431,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
this.state.alertParams.onConfirm();
|
||||
},
|
||||
confirm: this.state.alertParams.confirm
|
||||
}), react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_sidepanel_view_jsx__WEBPACK_IMPORTED_MODULE_11__["default"], {
|
||||
}) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_sidepanel_view_jsx__WEBPACK_IMPORTED_MODULE_11__["default"], {
|
||||
tinode: this.tinode,
|
||||
connected: this.state.connected,
|
||||
displayMobile: this.state.displayMobile,
|
||||
@@ -9486,6 +9515,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
callTopic: this.state.callTopic,
|
||||
callSeq: this.state.callSeq,
|
||||
callState: this.state.callState,
|
||||
callAudioOnly: this.state.callAudioOnly,
|
||||
onCallHangup: this.handleCallHangup,
|
||||
onCallInvite: this.handleCallInvite,
|
||||
onCallSendOffer: this.handleCallSendOffer,
|
||||
@@ -9718,7 +9748,7 @@ __webpack_require__.r(__webpack_exports__);
|
||||
|
||||
class Alert extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) {
|
||||
render() {
|
||||
return this.props.visible ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: "alert-container"
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: "alert"
|
||||
@@ -9746,7 +9776,7 @@ class Alert extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent)
|
||||
"type": 0,
|
||||
"value": "OK"
|
||||
}]
|
||||
}))))) : null;
|
||||
})))));
|
||||
}
|
||||
}
|
||||
;
|
||||
@@ -10662,6 +10692,8 @@ RING_SOUND.loop = true;
|
||||
const CALL_ENDED_SOUND = new Audio('audio/call-end.m4a');
|
||||
CALL_ENDED_SOUND.loop = true;
|
||||
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)({
|
||||
already_in_call: {
|
||||
id: "already_in_call",
|
||||
@@ -10677,13 +10709,17 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
this.state = {
|
||||
localStream: undefined,
|
||||
pc: undefined,
|
||||
dataChannel: undefined,
|
||||
previousOnInfo: undefined,
|
||||
waitingForPeer: false,
|
||||
callInitialSetupComplete: false
|
||||
callInitialSetupComplete: false,
|
||||
audioOnly: props.callAudioOnly,
|
||||
videoToggleInProgress: false,
|
||||
remoteVideoLive: false
|
||||
};
|
||||
this.localStreamConstraints = {
|
||||
audio: true,
|
||||
video: true
|
||||
video: !props.callAudioOnly
|
||||
};
|
||||
this.isOutgoingCall = props.callState == _constants_js__WEBPACK_IMPORTED_MODULE_4__.CALL_STATE_OUTGOING_INITATED;
|
||||
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.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
|
||||
this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
|
||||
this.toggleMedia = this.toggleMedia.bind(this);
|
||||
this.handleRemoteHangup = this.handleRemoteHangup.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() {
|
||||
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;
|
||||
stream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, stream);
|
||||
if (track.kind == 'video' && !this.localStreamConstraints.video) {
|
||||
track.stop();
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
});
|
||||
}
|
||||
onInfo(info) {
|
||||
@@ -10765,23 +10812,39 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
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() {
|
||||
if (this.state.localStream) {
|
||||
this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info');
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
navigator.mediaDevices.getUserMedia(this.localStreamConstraints).then(stream => {
|
||||
if (!this.localStreamConstraints.video) {
|
||||
stream.addTrack(this.emptyVideoTrack());
|
||||
}
|
||||
this.setState({
|
||||
localStream: stream,
|
||||
waitingForPeer: true
|
||||
});
|
||||
this.localRef.current.srcObject = stream;
|
||||
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);
|
||||
}
|
||||
stop() {
|
||||
@@ -10801,6 +10864,10 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
this.state.pc.onicegatheringstatechange = null;
|
||||
this.state.pc.onnegotiationneeded = null;
|
||||
this.state.pc.onicecandidateerror = null;
|
||||
this.state.pc.ondatachannel = null;
|
||||
if (this.state.dataChannel) {
|
||||
this.state.dataChannel.close();
|
||||
}
|
||||
this.state.pc.close();
|
||||
}
|
||||
this.setState({
|
||||
@@ -10826,6 +10893,41 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
el.srcObject = null;
|
||||
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() {
|
||||
const iceServers = this.props.tinode.getServerParam('iceServers', null);
|
||||
const pc = iceServers ? new RTCPeerConnection({
|
||||
@@ -10838,6 +10940,14 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
|
||||
pc.onicecandidateerror = this.handleIceCandidateErrorEvent;
|
||||
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({
|
||||
pc: pc,
|
||||
waitingForPeer: false
|
||||
@@ -10850,7 +10960,7 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
this.setState({
|
||||
callInitialSetupComplete: true
|
||||
}, _ => this.drainRemoteIceCandidatesCache());
|
||||
}).catch(this.reportError);
|
||||
}).catch(err => this.reportError(err));
|
||||
}
|
||||
reportError(err) {
|
||||
this.props.onError(err.message, 'err');
|
||||
@@ -10858,15 +10968,16 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
canSendOffer() {
|
||||
return this.isOutgoingCall || this.state.callInitialSetupComplete;
|
||||
}
|
||||
handleNegotiationNeededEvent() {
|
||||
handleNegotiationNeededEvent(event) {
|
||||
const pc = event.target;
|
||||
if (!this.canSendOffer()) {
|
||||
return;
|
||||
}
|
||||
this.state.pc.createOffer().then(offer => {
|
||||
return this.state.pc.setLocalDescription(offer);
|
||||
pc.createOffer().then(offer => {
|
||||
return pc.setLocalDescription(offer);
|
||||
}).then(_ => {
|
||||
this.props.onSendOffer(this.props.topic, this.props.seq, this.state.pc.localDescription.toJSON());
|
||||
}).catch(this.reportError);
|
||||
this.props.onSendOffer(this.props.topic, this.props.seq, pc.localDescription.toJSON());
|
||||
}).catch(err => this.reportError(err));
|
||||
}
|
||||
handleIceCandidateErrorEvent(event) {
|
||||
console.warn("ICE candidate error:", event);
|
||||
@@ -10879,14 +10990,24 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
handleNewICECandidateMsg(info) {
|
||||
const candidate = new RTCIceCandidate(info.payload);
|
||||
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 {
|
||||
this.remoteIceCandidatesCache.push(candidate);
|
||||
}
|
||||
}
|
||||
drainRemoteIceCandidatesCache() {
|
||||
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 = [];
|
||||
}
|
||||
@@ -10899,7 +11020,7 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
}
|
||||
}
|
||||
handleSignalingStateChangeEvent(event) {
|
||||
if (this.state.pc.signalingState == 'closed') {
|
||||
if (event.target.signalingState == 'closed') {
|
||||
this.handleCloseClick();
|
||||
}
|
||||
}
|
||||
@@ -10909,27 +11030,32 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
this.forceUpdate();
|
||||
}
|
||||
handleGetUserMediaError(e) {
|
||||
console.error("Error opening camera and/or microphone", e);
|
||||
switch (e.name) {
|
||||
case 'NotFoundError':
|
||||
this.reportError(e.message);
|
||||
this.reportError(e);
|
||||
break;
|
||||
case 'SecurityError':
|
||||
case 'PermissionDeniedError':
|
||||
break;
|
||||
default:
|
||||
this.reportError(e.message);
|
||||
console.error("Error opening your camera and/or microphone:", e.message);
|
||||
this.reportError(e);
|
||||
break;
|
||||
}
|
||||
this.handleCloseClick();
|
||||
}
|
||||
handleVideoOfferMsg(info) {
|
||||
let localStream = null;
|
||||
const pc = this.createPeerConnection();
|
||||
const pc = this.state.pc ? this.state.pc : this.createPeerConnection();
|
||||
const desc = new RTCSessionDescription(info.payload);
|
||||
pc.setRemoteDescription(desc).then(_ => {
|
||||
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
|
||||
}).then(stream => {
|
||||
let dummyVideo;
|
||||
if (!this.localStreamConstraints.video) {
|
||||
dummyVideo = this.emptyVideoTrack();
|
||||
stream.addTrack(dummyVideo);
|
||||
}
|
||||
localStream = stream;
|
||||
this.localRef.current.srcObject = stream;
|
||||
this.setState({
|
||||
@@ -10938,6 +11064,10 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
localStream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, localStream);
|
||||
});
|
||||
if (dummyVideo) {
|
||||
dummyVideo.stop();
|
||||
stream.removeTrack(dummyVideo);
|
||||
}
|
||||
}).then(_ => {
|
||||
return pc.createAnswer();
|
||||
}).then(answer => {
|
||||
@@ -10969,35 +11099,89 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
this.stop();
|
||||
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;
|
||||
stream.getTracks().forEach(track => {
|
||||
if (track.kind != kind) {
|
||||
return;
|
||||
}
|
||||
track.enabled = !track.enabled;
|
||||
const t = stream.getVideoTracks()[0];
|
||||
t.enabled = false;
|
||||
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 => {
|
||||
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() {
|
||||
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() {
|
||||
this.toggleMedia('audio');
|
||||
const stream = this.state.localStream;
|
||||
const t = stream.getAudioTracks()[0];
|
||||
t.enabled = !t.enabled;
|
||||
this.forceUpdate();
|
||||
}
|
||||
render() {
|
||||
const remoteActive = this.remoteRef.current && this.remoteRef.current.srcObject;
|
||||
const disabled = !(this.state.localStream && this.state.localStream.getTracks());
|
||||
const audioIcon = this.state.localStream && this.state.localStream.getAudioTracks()[0].enabled ? 'mic' : 'mic_off';
|
||||
const videoIcon = this.state.localStream && this.state.localStream.getVideoTracks()[0].enabled ? 'videocam' : 'videocam_off';
|
||||
const audioTracks = this.state.localStream && this.state.localStream.getAudioTracks();
|
||||
const videoTracks = !this.state.audioOnly && this.state.localStream && this.state.localStream.getVideoTracks();
|
||||
const disabled = !this.state.pc || !this.state.dataChannel || !(audioTracks && audioTracks[0]);
|
||||
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 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", {
|
||||
id: "video-container"
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
id: "video-container-panel"
|
||||
}, 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", {
|
||||
ref: this.localRef,
|
||||
autoPlay: true,
|
||||
@@ -11012,12 +11196,13 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
"value": "You"
|
||||
}]
|
||||
}))), 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", {
|
||||
ref: this.remoteRef,
|
||||
autoPlay: 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"
|
||||
}, peerTitle) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: `caller-card${pulseAnimation}`
|
||||
@@ -11116,7 +11301,7 @@ class CallStatus extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
|
||||
});
|
||||
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, {
|
||||
id: "call_in_progress",
|
||||
defaultMessage: [{
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
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
+51
-27
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
const PACKAGE_VERSION = "0.21.0-beta1";
|
||||
const PACKAGE_VERSION = "0.21.0-rc1";
|
||||
|
||||
Reference in New Issue
Block a user