Compare commits
25
Commits
4ad723b854
...
03e87697cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03e87697cf | ||
|
|
bccf0d3b6b | ||
|
|
36b7d1b721 | ||
|
|
2294410fb9 | ||
|
|
10140c6450 | ||
|
|
489a05663a | ||
|
|
f31a3ebdcc | ||
|
|
6360708255 | ||
|
|
24a87f3deb | ||
|
|
a8d7e6be50 | ||
|
|
b434c4ef17 | ||
|
|
0d33c0f0a8 | ||
|
|
e95aea17be | ||
|
|
d820da2ee2 | ||
|
|
692e6e1d8f | ||
|
|
6aca7e527e | ||
|
|
ae34795c2d | ||
|
|
310b352a08 | ||
|
|
b8e9ca8d0a | ||
|
|
7731c6700b | ||
|
|
563f82d265 | ||
|
|
10b24cc16e | ||
|
|
f6cd3048e1 | ||
|
|
0e11ae9382 | ||
|
|
fa78e678b9 |
@@ -1,2 +1,3 @@
|
|||||||
umd/* binary
|
umd/* binary
|
||||||
package-lock.json binary
|
package-lock.json binary
|
||||||
|
*.min.css binary
|
||||||
|
|||||||
+80
-1
@@ -400,6 +400,10 @@ div.trusted-badge {
|
|||||||
color: #388e3c; /* green 700 */
|
color: #388e3c; /* green 700 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.white {
|
||||||
|
color: #FFF!important; /* 100% white */
|
||||||
|
}
|
||||||
|
|
||||||
.red {
|
.red {
|
||||||
color: #D32F2F!important; /* red 700 */
|
color: #D32F2F!important; /* red 700 */
|
||||||
}
|
}
|
||||||
@@ -1878,6 +1882,7 @@ video {
|
|||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
|
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
|
||||||
background-color: rgb(244 244 244 / 66%);
|
background-color: rgb(244 244 244 / 66%);
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Left triangle placed bottom left flush. */
|
/* Left triangle placed bottom left flush. */
|
||||||
@@ -1985,6 +1990,7 @@ video {
|
|||||||
|
|
||||||
/* Inline and preview image styles */
|
/* Inline and preview image styles */
|
||||||
.inline-image, .image-preview {
|
.inline-image, .image-preview {
|
||||||
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 0.1rem;
|
border-radius: 0.1rem;
|
||||||
@@ -2022,7 +2028,7 @@ img.inline-image::before, img.image-preview::before {
|
|||||||
|
|
||||||
img.inline-image::before {
|
img.inline-image::before {
|
||||||
content: attr(alt);
|
content: attr(alt);
|
||||||
background-size: 2rem 2rem;
|
background-size: 3rem 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.image-preview::before {
|
img.image-preview::before {
|
||||||
@@ -2030,6 +2036,48 @@ img.image-preview::before {
|
|||||||
background-size: 10rem 10rem;
|
background-size: 10rem 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-image.video > img.inline-image::before, img.image-preview.video::before {
|
||||||
|
background: center no-repeat #ccc url("../img/broken_video.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-image.video > img.inline-image::before {
|
||||||
|
background-size: 3rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-video {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-video > img.inline-image::before {
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-video .duration {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.25rem;
|
||||||
|
left: 0.25rem;
|
||||||
|
font-size: 80%;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-video .play-control {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Attachment styles */
|
/* Attachment styles */
|
||||||
.attachment {
|
.attachment {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2470,6 +2518,9 @@ ul.tabbar li.active > a {
|
|||||||
.material-icons.big {
|
.material-icons.big {
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
}
|
}
|
||||||
|
.material-icons.bigger {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
.material-icons.huge {
|
.material-icons.huge {
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
}
|
}
|
||||||
@@ -2924,6 +2975,34 @@ ul.tabbar li.active > a {
|
|||||||
color: #666C;
|
color: #666C;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Drag-and-drop container.
|
||||||
|
*/
|
||||||
|
.drag-and-drop {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-and-drop .banner {
|
||||||
|
border: dashed grey 1px;
|
||||||
|
background-color: rgba(255,255,255,.8);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
/* Center text & font. */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Very wide monitor.
|
Very wide monitor.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Generated
+25
-25
@@ -13,7 +13,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-intl": "^6.2.1",
|
"react-intl": "^6.2.1",
|
||||||
"tinode-sdk": "^0.20.2",
|
"tinode-sdk": "^0.20.3",
|
||||||
"webm-duration-fix": "^1.0.4"
|
"webm-duration-fix": "^1.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -2423,9 +2423,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@grpc/grpc-js": {
|
"node_modules/@grpc/grpc-js": {
|
||||||
"version": "1.7.3",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.0.tgz",
|
||||||
"integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==",
|
"integrity": "sha512-ySMTXQuMvvswoobvN+0LsaPf7ITO2JVfJmHxQKI4cGehNrrUms+n81BlHEX7Hl/LExji6XE3fnI9U04GSkRruA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/proto-loader": "^0.7.0",
|
"@grpc/proto-loader": "^0.7.0",
|
||||||
"@types/node": ">=12.12.47"
|
"@types/node": ">=12.12.47"
|
||||||
@@ -2435,9 +2435,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
|
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
|
||||||
"version": "0.7.3",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz",
|
||||||
"integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==",
|
"integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/long": "^4.0.1",
|
"@types/long": "^4.0.1",
|
||||||
"lodash.camelcase": "^4.3.0",
|
"lodash.camelcase": "^4.3.0",
|
||||||
@@ -6266,9 +6266,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/tinode-sdk": {
|
"node_modules/tinode-sdk": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/tinode-sdk/-/tinode-sdk-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/tinode-sdk/-/tinode-sdk-0.20.3.tgz",
|
||||||
"integrity": "sha512-aazufXwxwFx6VMByRMe7LeUgqDKiCEqRuf8DJO+mD0CdL1FVQP2mcC91ZQmORSUXtBX5lWYtl6TDPDZOVCS/lg=="
|
"integrity": "sha512-xJORATgCwCsILp2JmCvVRZxlxdN9bXjtRETHfFrBU40HxoP9z6Nk48KnDdTVvzSxwHnfwtxzj9B1KUl6Q1tRaQ=="
|
||||||
},
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
@@ -6313,9 +6313,9 @@
|
|||||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.9.3",
|
"version": "4.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
|
||||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -8518,18 +8518,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@grpc/grpc-js": {
|
"@grpc/grpc-js": {
|
||||||
"version": "1.7.3",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.0.tgz",
|
||||||
"integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==",
|
"integrity": "sha512-ySMTXQuMvvswoobvN+0LsaPf7ITO2JVfJmHxQKI4cGehNrrUms+n81BlHEX7Hl/LExji6XE3fnI9U04GSkRruA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@grpc/proto-loader": "^0.7.0",
|
"@grpc/proto-loader": "^0.7.0",
|
||||||
"@types/node": ">=12.12.47"
|
"@types/node": ">=12.12.47"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/proto-loader": {
|
"@grpc/proto-loader": {
|
||||||
"version": "0.7.3",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz",
|
||||||
"integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==",
|
"integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/long": "^4.0.1",
|
"@types/long": "^4.0.1",
|
||||||
"lodash.camelcase": "^4.3.0",
|
"lodash.camelcase": "^4.3.0",
|
||||||
@@ -11350,9 +11350,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"tinode-sdk": {
|
"tinode-sdk": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/tinode-sdk/-/tinode-sdk-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/tinode-sdk/-/tinode-sdk-0.20.3.tgz",
|
||||||
"integrity": "sha512-aazufXwxwFx6VMByRMe7LeUgqDKiCEqRuf8DJO+mD0CdL1FVQP2mcC91ZQmORSUXtBX5lWYtl6TDPDZOVCS/lg=="
|
"integrity": "sha512-xJORATgCwCsILp2JmCvVRZxlxdN9bXjtRETHfFrBU40HxoP9z6Nk48KnDdTVvzSxwHnfwtxzj9B1KUl6Q1tRaQ=="
|
||||||
},
|
},
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
@@ -11388,9 +11388,9 @@
|
|||||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.9.3",
|
"version": "4.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
|
||||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"unicode-canonical-property-names-ecmascript": {
|
"unicode-canonical-property-names-ecmascript": {
|
||||||
|
|||||||
+1
-3
@@ -2,7 +2,6 @@
|
|||||||
"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-beta1",
|
||||||
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/tinode/example-react-js.git"
|
"url": "https://github.com/tinode/example-react-js.git"
|
||||||
@@ -68,7 +67,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-intl": "^6.2.1",
|
"react-intl": "^6.2.1",
|
||||||
"tinode-sdk": "^0.20.2",
|
"tinode-sdk": "^0.21.0-beta1",
|
||||||
"webm-duration-fix": "^1.0.4"
|
"webm-duration-fix": "^1.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -82,7 +81,6 @@
|
|||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"cssnano": "^5.1.14",
|
"cssnano": "^5.1.14",
|
||||||
"glob": "^8.0.3",
|
"glob": "^8.0.3",
|
||||||
"mkdirp": "^1.0.4",
|
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
"postcss-cli": "^10.0.0",
|
"postcss-cli": "^10.0.0",
|
||||||
"webpack": "^5.75.0",
|
"webpack": "^5.75.0",
|
||||||
|
|||||||
+1
-7
@@ -5,13 +5,11 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const globSync = require('glob').sync;
|
const globSync = require('glob').sync;
|
||||||
const mkdirpSync = require('mkdirp').sync;
|
|
||||||
|
|
||||||
const EXTRACTED_STRINGS = './src/i18n/ex/base-en.json';
|
const EXTRACTED_STRINGS = './src/i18n/ex/base-en.json';
|
||||||
const BASE_LANG = 'en';
|
const BASE_LANG = 'en';
|
||||||
const LANG_PATTERN = './src/i18n/*.json';
|
const LANG_PATTERN = './src/i18n/*.json';
|
||||||
const LANG_PATTERN_ONE = './src/i18n/%s.json';
|
const LANG_PATTERN_ONE = './src/i18n/%s.json';
|
||||||
const FINAL_FILE = './src/messages.json';
|
|
||||||
const OUTPUT_PATTERN = './src/i18n.min/%s.json';
|
const OUTPUT_PATTERN = './src/i18n.min/%s.json';
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
@@ -123,11 +121,7 @@ const messages = globSync(LANG_PATTERN)
|
|||||||
return { ...collection, ...descriptors };
|
return { ...collection, ...descriptors };
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// Legacy: Write a single file with all translations combined.
|
// Write minified translation files, one per language.
|
||||||
mkdirpSync(path.dirname(FINAL_FILE));
|
|
||||||
fs.writeFileSync(FINAL_FILE, JSON.stringify(messages));
|
|
||||||
|
|
||||||
// New way: write minified ptraslation files.
|
|
||||||
for (let lang in messages) {
|
for (let lang in messages) {
|
||||||
fs.writeFileSync(printf(OUTPUT_PATTERN, lang), JSON.stringify(messages[lang]));
|
fs.writeFileSync(printf(OUTPUT_PATTERN, lang), JSON.stringify(messages[lang]));
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,7 +1,7 @@
|
|||||||
import { PACKAGE_VERSION } from './version.js';
|
import { PACKAGE_VERSION } from './version.js';
|
||||||
|
|
||||||
// Name of this application, used in the User-Agent.
|
// Name of this application, used in the User-Agent.
|
||||||
export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.17');
|
export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.21');
|
||||||
|
|
||||||
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
|
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
|
||||||
export const API_KEY = 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K';
|
export const API_KEY = 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K';
|
||||||
@@ -76,6 +76,9 @@ export const MAX_IMAGE_DIM = 1024;
|
|||||||
// Linear dimensions of image preview: shrink image under this size for previews.
|
// Linear dimensions of image preview: shrink image under this size for previews.
|
||||||
export const IMAGE_PREVIEW_DIM = 64;
|
export const IMAGE_PREVIEW_DIM = 64;
|
||||||
|
|
||||||
|
// Linear dimensions of video screencapture: shrink image under this size for previews.
|
||||||
|
export const VIDEO_PREVIEW_DIM = 96;
|
||||||
|
|
||||||
// Linear dimensions of image thumbnail: shrink image under this size for thumbnails in reply quote previews.
|
// Linear dimensions of image thumbnail: shrink image under this size for thumbnails in reply quote previews.
|
||||||
export const IMAGE_THUMBNAIL_DIM = 36;
|
export const IMAGE_THUMBNAIL_DIM = 36;
|
||||||
|
|
||||||
|
|||||||
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
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
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
+63
-7
@@ -307,13 +307,6 @@
|
|||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"translation": "Inhalt gelöscht",
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted",
|
|
||||||
"missing": false,
|
|
||||||
"obsolete": false
|
|
||||||
},
|
|
||||||
"download_action": {
|
"download_action": {
|
||||||
"translation": "herunterladen",
|
"translation": "herunterladen",
|
||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
@@ -1472,5 +1465,68 @@
|
|||||||
"description": "Marker indicating that the message was edited",
|
"description": "Marker indicating that the message was edited",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"translation": "Bild hinzufügen",
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"translation": "Datei anhängen",
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"translation": "Aufnahme löschen",
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"translation": "Wiedergabe pausieren",
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"translation": "Aufnahme abspielen",
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"translation": "Sprachnachricht aufnehmen",
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"translation": "Wiedergabe fortsetzen",
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"translation": "Nachricht senden",
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-7
@@ -307,13 +307,6 @@
|
|||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"translation": "content deleted",
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted",
|
|
||||||
"missing": false,
|
|
||||||
"obsolete": false
|
|
||||||
},
|
|
||||||
"download_action": {
|
"download_action": {
|
||||||
"translation": "download",
|
"translation": "download",
|
||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
@@ -1472,5 +1465,68 @@
|
|||||||
"description": "Marker indicating that the message was edited",
|
"description": "Marker indicating that the message was edited",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"translation": "Add image",
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"translation": "Attach file",
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"translation": "Delete",
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"translation": "Pause",
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"translation": "Play",
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"translation": "Record voice message",
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"translation": "Resume",
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"translation": "Send message",
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"translation": "Drag file here",
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-7
@@ -307,13 +307,6 @@
|
|||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"translation": "este mensaje fue eliminado",
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted",
|
|
||||||
"missing": false,
|
|
||||||
"obsolete": false
|
|
||||||
},
|
|
||||||
"download_action": {
|
"download_action": {
|
||||||
"translation": "descargar",
|
"translation": "descargar",
|
||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
@@ -1472,5 +1465,68 @@
|
|||||||
"description": "Marker indicating that the message was edited",
|
"description": "Marker indicating that the message was edited",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"translation": "Añadir imagen",
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"translation": "Adjuntar archivo",
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"translation": "Eliminar grabación",
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"translation": "Pausar reproducción",
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"translation": "Reproducir grabación",
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"translation": "Grabar mensaje de voz",
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"translation": "Reanudar la reproducción",
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"translation": "Enviar mensaje",
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,10 +215,6 @@
|
|||||||
"defaultMessage": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
|
"defaultMessage": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
|
||||||
"description": "Alert dialog warning when hard-deleting all messages."
|
"description": "Alert dialog warning when hard-deleting all messages."
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted"
|
|
||||||
},
|
|
||||||
"description_editing_placeholder": {
|
"description_editing_placeholder": {
|
||||||
"defaultMessage": "Description (optional)",
|
"defaultMessage": "Description (optional)",
|
||||||
"description": "Placeholder for editing topic description"
|
"description": "Placeholder for editing topic description"
|
||||||
@@ -243,6 +239,10 @@
|
|||||||
"defaultMessage": "Unsupported",
|
"defaultMessage": "Unsupported",
|
||||||
"description": "Unsupported entity in drafty"
|
"description": "Unsupported entity in drafty"
|
||||||
},
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner"
|
||||||
|
},
|
||||||
"editing_message": {
|
"editing_message": {
|
||||||
"defaultMessage": "Editing",
|
"defaultMessage": "Editing",
|
||||||
"description": "Title over message editing preview"
|
"description": "Title over message editing preview"
|
||||||
@@ -298,6 +298,38 @@
|
|||||||
"defaultMessage": "Group or User ID",
|
"defaultMessage": "Group or User ID",
|
||||||
"description": "Prompt for entering user or group ID"
|
"description": "Prompt for entering user or group ID"
|
||||||
},
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image"
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file"
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio"
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback"
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback"
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message"
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback"
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message"
|
||||||
|
},
|
||||||
"image_caption_prompt": {
|
"image_caption_prompt": {
|
||||||
"defaultMessage": "Image caption",
|
"defaultMessage": "Image caption",
|
||||||
"description": "Prompt in SendMessage for attached image"
|
"description": "Prompt in SendMessage for attached image"
|
||||||
|
|||||||
+63
-7
@@ -307,13 +307,6 @@
|
|||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"translation": "contenu supprimé",
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted",
|
|
||||||
"missing": false,
|
|
||||||
"obsolete": false
|
|
||||||
},
|
|
||||||
"download_action": {
|
"download_action": {
|
||||||
"translation": "télécharger",
|
"translation": "télécharger",
|
||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
@@ -1472,5 +1465,68 @@
|
|||||||
"description": "Marker indicating that the message was edited",
|
"description": "Marker indicating that the message was edited",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"translation": "Ajouter une image",
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"translation": "Pièce jointe",
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"translation": "Supprimer l'enregistrement",
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"translation": "Interrompre la lecture",
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"translation": "Lire l'enregistrement",
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"translation": "Enregistrer un message vocal",
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"translation": "Reprendre la lecture",
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"translation": "Envoyer le message",
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-7
@@ -307,13 +307,6 @@
|
|||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"translation": "내용이 삭제되었습니다",
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted",
|
|
||||||
"missing": false,
|
|
||||||
"obsolete": false
|
|
||||||
},
|
|
||||||
"download_action": {
|
"download_action": {
|
||||||
"translation": "다운로드",
|
"translation": "다운로드",
|
||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
@@ -1472,5 +1465,68 @@
|
|||||||
"description": "Marker indicating that the message was edited",
|
"description": "Marker indicating that the message was edited",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"translation": "이미지 추가",
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"translation": "파일을 첨부",
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"translation": "녹음 삭제",
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"translation": "재생 일시 중지",
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"translation": "녹음 재생",
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"translation": "음성 메시지 녹음",
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"translation": "재생 재개",
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"translation": "문자 보내",
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-7
@@ -307,13 +307,6 @@
|
|||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"translation": "conținut șters",
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted",
|
|
||||||
"missing": false,
|
|
||||||
"obsolete": false
|
|
||||||
},
|
|
||||||
"download_action": {
|
"download_action": {
|
||||||
"translation": "download",
|
"translation": "download",
|
||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
@@ -1472,5 +1465,68 @@
|
|||||||
"description": "Marker indicating that the message was edited",
|
"description": "Marker indicating that the message was edited",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"translation": "Adauga imagine",
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"translation": "Ataseaza fisier",
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"translation": "Ștergeți înregistrarea",
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"translation": "Întrerupeți redarea",
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"translation": "Redați înregistrarea",
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"translation": "Înregistrați mesajul vocal",
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"translation": "Reluați redarea",
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"translation": "Trimite mesaj",
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-7
@@ -307,13 +307,6 @@
|
|||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"translation": "удалено",
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted",
|
|
||||||
"missing": false,
|
|
||||||
"obsolete": false
|
|
||||||
},
|
|
||||||
"download_action": {
|
"download_action": {
|
||||||
"translation": "скачать",
|
"translation": "скачать",
|
||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
@@ -1472,5 +1465,68 @@
|
|||||||
"description": "Marker indicating that the message was edited",
|
"description": "Marker indicating that the message was edited",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"translation": "Добавить изображение",
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"translation": "Прикрепить файл",
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"translation": "Удалить запись",
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"translation": "Пауза воспроизведения",
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"translation": "Воспроизвести запись",
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"translation": "Записать голосовое сообщение",
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"translation": "Продолжить",
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"translation": "Отправить сообщение",
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-7
@@ -307,13 +307,6 @@
|
|||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"translation": "內容已被刪除",
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted",
|
|
||||||
"missing": false,
|
|
||||||
"obsolete": false
|
|
||||||
},
|
|
||||||
"download_action": {
|
"download_action": {
|
||||||
"translation": "下載",
|
"translation": "下載",
|
||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
@@ -1472,5 +1465,68 @@
|
|||||||
"description": "Marker indicating that the message was edited",
|
"description": "Marker indicating that the message was edited",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"translation": "添加圖片",
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"translation": "附加文件",
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"translation": "刪除錄音",
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"translation": "暫停播放",
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"translation": "播放錄音",
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"translation": "錄製語音消息",
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"translation": "恢復播放",
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"translation": "發信息",
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-7
@@ -307,13 +307,6 @@
|
|||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"deleted_content": {
|
|
||||||
"translation": "内容已删除",
|
|
||||||
"defaultMessage": "content deleted",
|
|
||||||
"description": "Shown when messages are deleted",
|
|
||||||
"missing": false,
|
|
||||||
"obsolete": false
|
|
||||||
},
|
|
||||||
"download_action": {
|
"download_action": {
|
||||||
"translation": "下载",
|
"translation": "下载",
|
||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
@@ -1472,5 +1465,68 @@
|
|||||||
"description": "Marker indicating that the message was edited",
|
"description": "Marker indicating that the message was edited",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_add_image": {
|
||||||
|
"translation": "添加图片",
|
||||||
|
"defaultMessage": "Add image",
|
||||||
|
"description": "Icon tool tip for attaching an image",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_attach_file": {
|
||||||
|
"translation": "附加文件",
|
||||||
|
"defaultMessage": "Attach file",
|
||||||
|
"description": "Icon tool tip for attaching a file",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_delete": {
|
||||||
|
"translation": "删除录音",
|
||||||
|
"defaultMessage": "Delete recording",
|
||||||
|
"description": "Icon tool tip for deleting recorded audio",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_pause": {
|
||||||
|
"translation": "暂停播放",
|
||||||
|
"defaultMessage": "Pause playback",
|
||||||
|
"description": "Icon tool tip for pausing audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_play": {
|
||||||
|
"translation": "播放录音",
|
||||||
|
"defaultMessage": "Play recording",
|
||||||
|
"description": "Icon tool tip for starting audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_record_voice": {
|
||||||
|
"translation": "录制语音信息",
|
||||||
|
"defaultMessage": "Record voice message",
|
||||||
|
"description": "Icon tool tip for recording a voice message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_resume": {
|
||||||
|
"translation": "恢复播放",
|
||||||
|
"defaultMessage": "Resume playback",
|
||||||
|
"description": "Icon tool tip for resuming audio playback",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"icon_title_send": {
|
||||||
|
"translation": "发信息",
|
||||||
|
"defaultMessage": "Send message",
|
||||||
|
"description": "Icon tool tip for sending a message",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drag_file": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Drag file here",
|
||||||
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-13
@@ -6,12 +6,10 @@ import { createRoot } from 'react-dom/client';
|
|||||||
|
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import allMessages from './messages.json';
|
|
||||||
import TinodeWeb from './views/tinode-web.jsx';
|
import TinodeWeb from './views/tinode-web.jsx';
|
||||||
import HashNavigation from './lib/navigation.js';
|
import HashNavigation from './lib/navigation.js';
|
||||||
|
|
||||||
/*
|
// Allow loading translation strings for just one language.
|
||||||
// Load just one language.
|
|
||||||
const messageLoader = {
|
const messageLoader = {
|
||||||
'de': _ => import('./i18n.min/de.json'),
|
'de': _ => import('./i18n.min/de.json'),
|
||||||
'en': _ => import('./i18n.min/en.json'),
|
'en': _ => import('./i18n.min/en.json'),
|
||||||
@@ -23,7 +21,6 @@ const messageLoader = {
|
|||||||
'zh': _ => import('./i18n.min/zh.json'),
|
'zh': _ => import('./i18n.min/zh.json'),
|
||||||
'zh-TW': _ => import('./i18n.min/zh-TW.json')
|
'zh-TW': _ => import('./i18n.min/zh-TW.json')
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
// Detect human language to use in the UI:
|
// Detect human language to use in the UI:
|
||||||
// Check parameters from URL hash #?hl=ru, then browser, then use 'en' as a fallback.
|
// Check parameters from URL hash #?hl=ru, then browser, then use 'en' as a fallback.
|
||||||
@@ -40,18 +37,16 @@ const normalized = language.replace('_', '-');
|
|||||||
const baseLanguage = normalized.split('-')[0].toLowerCase();
|
const baseLanguage = normalized.split('-')[0].toLowerCase();
|
||||||
|
|
||||||
// Try the full locale first, then the locale without the region code, fallback to 'en'.
|
// Try the full locale first, then the locale without the region code, fallback to 'en'.
|
||||||
// const htmlLang = messageLoader[normalized] ? language : messageLoader[baseLanguage] ? baseLanguage : 'en';
|
const htmlLang = messageLoader[normalized] ? language : messageLoader[baseLanguage] ? baseLanguage : 'en';
|
||||||
// const messages = await messageLoader[htmlLang]();
|
|
||||||
const htmlLang = allMessages[normalized] ? language : allMessages[baseLanguage] ? baseLanguage : 'en';
|
|
||||||
const messages = allMessages[htmlLang];
|
|
||||||
|
|
||||||
// Set lang attribute of the HTML element: <html lang="XX">
|
// Set lang attribute of the HTML element: <html lang="XX">
|
||||||
document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
|
document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
|
||||||
|
|
||||||
// Render the app.
|
// Render the app.
|
||||||
const root = createRoot(document.getElementById('mountPoint'));
|
const root = createRoot(document.getElementById('mountPoint'));
|
||||||
root.render(
|
messageLoader[htmlLang]().then(messages =>
|
||||||
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
|
root.render(
|
||||||
<TinodeWeb />
|
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
|
||||||
</IntlProvider>
|
<TinodeWeb />
|
||||||
);
|
</IntlProvider>
|
||||||
|
));
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export function imageCrop(mime, objURL, left, top, width, height, scale) {
|
|||||||
export function fileToBase64(file) {
|
export function fileToBase64(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onerror = (evt) => {
|
reader.onerror = _ => {
|
||||||
reject(reader.error);
|
reject(reader.error);
|
||||||
};
|
};
|
||||||
reader.onload = _ => {
|
reader.onload = _ => {
|
||||||
|
|||||||
+97
-49
@@ -6,6 +6,7 @@ import { Drafty } from 'tinode-sdk';
|
|||||||
import AudioPlayer from '../widgets/audio-player.jsx'
|
import AudioPlayer from '../widgets/audio-player.jsx'
|
||||||
import CallMessage from '../widgets/call-message.jsx'
|
import CallMessage from '../widgets/call-message.jsx'
|
||||||
import CallStatus from '../widgets/call-status.jsx';
|
import CallStatus from '../widgets/call-status.jsx';
|
||||||
|
import InlineVideo from '../widgets/inline-video.jsx';
|
||||||
import LazyImage from '../widgets/lazy-image.jsx'
|
import LazyImage from '../widgets/lazy-image.jsx'
|
||||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||||
|
|
||||||
@@ -37,57 +38,14 @@ const messages = defineMessages({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Additional processing of image data.
|
|
||||||
function handleImageData(el, data, attr) {
|
|
||||||
if (!data) {
|
|
||||||
attr.src = 'img/broken_image.png';
|
|
||||||
attr.style = {
|
|
||||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
|
||||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
|
||||||
};
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
attr.className = 'inline-image';
|
|
||||||
const dim = fitImageSize(data.width, data.height,
|
|
||||||
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
|
||||||
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
|
|
||||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
|
||||||
attr.style = {
|
|
||||||
width: dim.dstWidth + 'px',
|
|
||||||
height: dim.dstHeight + 'px',
|
|
||||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
|
||||||
minWidth: dim.dstWidth + 'px',
|
|
||||||
minHeight: dim.dstHeight + 'px'
|
|
||||||
};
|
|
||||||
if (!Drafty.isProcessing(data)) {
|
|
||||||
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
|
|
||||||
attr.alt = data.name;
|
|
||||||
if (attr.src) {
|
|
||||||
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
|
|
||||||
// Allow previews for large enough images.
|
|
||||||
attr.onClick = this.onImagePreview;
|
|
||||||
attr.className += ' image-clickable';
|
|
||||||
}
|
|
||||||
attr.loading = 'lazy';
|
|
||||||
} else {
|
|
||||||
attr.src = 'img/broken_image.png';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use custom element instead of <img>.
|
|
||||||
el = UploadingImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
|
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
|
||||||
// 'this' must contain:
|
// 'this' must contain:
|
||||||
// viewportWidth: this.props.viewportWidth;
|
// viewportWidth:
|
||||||
// authorizeURL: this.props.tinode.authorizeURL
|
// authorizeURL:
|
||||||
// onImagePreview: this.handleImagePreview
|
// onImagePreview:
|
||||||
// onFormButtonClick: this.handleFormButtonClick
|
// onVideoPreview:
|
||||||
// onQuoteClick: this.handleQuoteClick (optional)
|
// onFormButtonClick:
|
||||||
|
// onQuoteClick:
|
||||||
export function fullFormatter(style, data, values, key, stack) {
|
export function fullFormatter(style, data, values, key, stack) {
|
||||||
if (stack.includes('QQ')) {
|
if (stack.includes('QQ')) {
|
||||||
return quoteFormatter.call(this, style, data, values, key);
|
return quoteFormatter.call(this, style, data, values, key);
|
||||||
@@ -175,6 +133,12 @@ export function fullFormatter(style, data, values, key, stack) {
|
|||||||
attr.duration = data.duration;
|
attr.duration = data.duration;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'VD':
|
||||||
|
// Additional processing for videos.
|
||||||
|
el = handleVideoData.call(this, el, data, attr);
|
||||||
|
// Video element cannot have content.
|
||||||
|
values = null;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (!el) {
|
if (!el) {
|
||||||
// Unknown element.
|
// Unknown element.
|
||||||
@@ -195,6 +159,90 @@ export function fullFormatter(style, data, values, key, stack) {
|
|||||||
return React.createElement(el, attr, values);
|
return React.createElement(el, attr, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Additional processing of image data.
|
||||||
|
function handleImageData(el, data, attr) {
|
||||||
|
if (!data) {
|
||||||
|
attr.src = 'img/broken_image.png';
|
||||||
|
attr.style = {
|
||||||
|
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||||
|
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||||
|
};
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
attr.className = 'inline-image';
|
||||||
|
const dim = fitImageSize(data.width, data.height,
|
||||||
|
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
||||||
|
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
|
||||||
|
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||||
|
attr.style = {
|
||||||
|
width: dim.dstWidth + 'px',
|
||||||
|
height: dim.dstHeight + 'px',
|
||||||
|
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||||
|
minWidth: dim.dstWidth + 'px',
|
||||||
|
minHeight: dim.dstHeight + 'px'
|
||||||
|
};
|
||||||
|
if (!Drafty.isProcessing(data)) {
|
||||||
|
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
|
||||||
|
attr.alt = data.name;
|
||||||
|
if (attr.src) {
|
||||||
|
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
|
||||||
|
// Allow previews for large enough images.
|
||||||
|
attr.onClick = this.onImagePreview;
|
||||||
|
attr.className += ' image-clickable';
|
||||||
|
}
|
||||||
|
attr.loading = 'lazy';
|
||||||
|
} else {
|
||||||
|
attr.src = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use custom element instead of <img>.
|
||||||
|
el = UploadingImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional processing of image data.
|
||||||
|
function handleVideoData(el, data, attr) {
|
||||||
|
if (!data) {
|
||||||
|
attr.src = 'img/broken_video.png';
|
||||||
|
attr.style = {
|
||||||
|
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||||
|
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||||
|
};
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
attr.className = 'inline-image';
|
||||||
|
const dim = fitImageSize(data.width, data.height,
|
||||||
|
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
||||||
|
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
|
||||||
|
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||||
|
attr.style = {
|
||||||
|
width: dim.dstWidth + 'px',
|
||||||
|
height: dim.dstHeight + 'px',
|
||||||
|
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||||
|
minWidth: dim.dstWidth + 'px',
|
||||||
|
minHeight: dim.dstHeight + 'px'
|
||||||
|
};
|
||||||
|
if (!Drafty.isProcessing(data)) {
|
||||||
|
attr.alt = data.name;
|
||||||
|
if (data.ref || data.val) {
|
||||||
|
attr.onClick = this.onVideoPreview;
|
||||||
|
attr.loading = 'lazy';
|
||||||
|
} else {
|
||||||
|
attr.src = null;
|
||||||
|
}
|
||||||
|
el = InlineVideo;
|
||||||
|
} else {
|
||||||
|
// Use custom element instead of <img>.
|
||||||
|
el = UploadingImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
// Converts Drafty object into a one-line preview. 'this' is set by the caller.
|
// Converts Drafty object into a one-line preview. 'this' is set by the caller.
|
||||||
// 'this' must contain:
|
// 'this' must contain:
|
||||||
// formatMessage: this.props.intl.formatMessage
|
// formatMessage: this.props.intl.formatMessage
|
||||||
|
|||||||
+22
-3
@@ -16,16 +16,35 @@ export function shortDateFormat(then, locale) {
|
|||||||
return then.toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'});
|
return then.toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Representation a date relative to now.
|
||||||
|
export function relativeDateFormat(then, locale) {
|
||||||
|
locale = locale || window.navigator.userLanguage || window.navigator.language;
|
||||||
|
const now = new Date();
|
||||||
|
const thenDays = Math.floor((then.getTime() - then.getTimezoneOffset() * 60_000) / 86_400_000);
|
||||||
|
const nowDays = Math.floor((now.getTime() - now.getTimezoneOffset() * 60_000) / 86_400_000);
|
||||||
|
const diff = thenDays - nowDays;
|
||||||
|
if (Math.abs(diff) < 2) {
|
||||||
|
// Today or yesterday.
|
||||||
|
return new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(diff, 'day')
|
||||||
|
}
|
||||||
|
// More than two days: just show the date.
|
||||||
|
return new Intl.DateTimeFormat(locale).format(then);
|
||||||
|
}
|
||||||
|
|
||||||
// Convert seconds to [hours:]minutes:seconds, i.e. 156 sec -> 2:36, 3756 sec -> 1:02:36.
|
// Convert seconds to [hours:]minutes:seconds, i.e. 156 sec -> 2:36, 3756 sec -> 1:02:36.
|
||||||
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
|
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
|
||||||
export function secondsToTime(seconds, fixedMin) {
|
export function secondsToTime(seconds, fixedMin) {
|
||||||
let min = Math.floor(seconds / 60) | 0;
|
if (typeof seconds != 'number') {
|
||||||
let hours = Math.floor(min / 60) | 0;
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let min = (Math.floor(seconds / 60) | 0) % 60;
|
||||||
|
let hours = Math.floor(seconds / 36000) | 0;
|
||||||
|
|
||||||
if (fixedMin || hours > 0) {
|
if (fixedMin || hours > 0) {
|
||||||
min = min < 10 ? `0${min}` : min;
|
min = min < 10 ? `0${min}` : min;
|
||||||
}
|
}
|
||||||
let sec = (seconds % 60) | 0;
|
let sec = (seconds | 0) % 60;
|
||||||
sec = sec < 10 ? `0${sec}` : sec;
|
sec = sec < 10 ? `0${sec}` : sec;
|
||||||
if (hours == 0) {
|
if (hours == 0) {
|
||||||
return `${min}:${sec}`;
|
return `${min}:${sec}`;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+227
-80
@@ -9,6 +9,7 @@ import CallPanel from '../widgets/call-panel.jsx';
|
|||||||
import ChatMessage from '../widgets/chat-message.jsx';
|
import ChatMessage from '../widgets/chat-message.jsx';
|
||||||
import ContactBadges from '../widgets/contact-badges.jsx';
|
import ContactBadges from '../widgets/contact-badges.jsx';
|
||||||
import DocPreview from '../widgets/doc-preview.jsx';
|
import DocPreview from '../widgets/doc-preview.jsx';
|
||||||
|
import DragAndDrop from '../widgets/drag-and-drop.jsx'
|
||||||
import ErrorPanel from '../widgets/error-panel.jsx';
|
import ErrorPanel from '../widgets/error-panel.jsx';
|
||||||
import GroupSubs from '../widgets/group-subs.jsx';
|
import GroupSubs from '../widgets/group-subs.jsx';
|
||||||
import ImagePreview from '../widgets/image-preview.jsx';
|
import ImagePreview from '../widgets/image-preview.jsx';
|
||||||
@@ -18,14 +19,15 @@ import LoadSpinner from '../widgets/load-spinner.jsx';
|
|||||||
import LogoView from './logo-view.jsx';
|
import LogoView from './logo-view.jsx';
|
||||||
import MetaMessage from '../widgets/meta-message.jsx';
|
import MetaMessage from '../widgets/meta-message.jsx';
|
||||||
import SendMessage from '../widgets/send-message.jsx';
|
import SendMessage from '../widgets/send-message.jsx';
|
||||||
|
import VideoPreview from '../widgets/video-preview.jsx';
|
||||||
|
|
||||||
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
|
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
|
||||||
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
|
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
|
||||||
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
|
READ_DELAY, QUOTED_REPLY_LENGTH, VIDEO_PREVIEW_DIM } from '../config.js';
|
||||||
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
|
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
|
||||||
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
||||||
import HashNavigation from '../lib/navigation.js';
|
import HashNavigation from '../lib/navigation.js';
|
||||||
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
import { bytesToHumanSize, relativeDateFormat, shortDateFormat } from '../lib/strformat.js';
|
||||||
|
|
||||||
// Run timer with this frequency (ms) for checking notification queue.
|
// Run timer with this frequency (ms) for checking notification queue.
|
||||||
const NOTIFICATION_EXEC_INTERVAL = 300;
|
const NOTIFICATION_EXEC_INTERVAL = 300;
|
||||||
@@ -67,6 +69,11 @@ const messages = defineMessages({
|
|||||||
id: 'editing_message',
|
id: 'editing_message',
|
||||||
defaultMessage: 'Editing',
|
defaultMessage: 'Editing',
|
||||||
description: 'Title over message editing preview'
|
description: 'Title over message editing preview'
|
||||||
|
},
|
||||||
|
drag_file: {
|
||||||
|
id: 'drag_file',
|
||||||
|
defaultMessage: 'Drag file here',
|
||||||
|
description: 'Prompt on the file drag-n-drop overlay banner'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,6 +111,7 @@ class MessagesView extends React.Component {
|
|||||||
this.sendMessage = this.sendMessage.bind(this);
|
this.sendMessage = this.sendMessage.bind(this);
|
||||||
this.retrySend = this.retrySend.bind(this);
|
this.retrySend = this.retrySend.bind(this);
|
||||||
this.sendImageAttachment = this.sendImageAttachment.bind(this);
|
this.sendImageAttachment = this.sendImageAttachment.bind(this);
|
||||||
|
this.sendVideoAttachment = this.sendVideoAttachment.bind(this);
|
||||||
this.sendFileAttachment = this.sendFileAttachment.bind(this);
|
this.sendFileAttachment = this.sendFileAttachment.bind(this);
|
||||||
this.sendAudioAttachment = this.sendAudioAttachment.bind(this);
|
this.sendAudioAttachment = this.sendAudioAttachment.bind(this);
|
||||||
this.sendKeyPress = this.sendKeyPress.bind(this);
|
this.sendKeyPress = this.sendKeyPress.bind(this);
|
||||||
@@ -123,11 +131,12 @@ class MessagesView extends React.Component {
|
|||||||
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
||||||
this.handleEnablePeer = this.handleEnablePeer.bind(this);
|
this.handleEnablePeer = this.handleEnablePeer.bind(this);
|
||||||
this.handleAttachFile = this.handleAttachFile.bind(this);
|
this.handleAttachFile = this.handleAttachFile.bind(this);
|
||||||
this.handleAttachImage = this.handleAttachImage.bind(this);
|
this.handleAttachImageOrVideo = this.handleAttachImageOrVideo.bind(this);
|
||||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||||
this.postReadNotification = this.postReadNotification.bind(this);
|
this.postReadNotification = this.postReadNotification.bind(this);
|
||||||
this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
|
this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
|
||||||
this.goToLatestMessage = this.goToLatestMessage.bind(this);
|
this.goToLatestMessage = this.goToLatestMessage.bind(this);
|
||||||
|
this.handleFileDrop = this.handleFileDrop.bind(this);
|
||||||
|
|
||||||
this.handlePickReply = this.handlePickReply.bind(this);
|
this.handlePickReply = this.handlePickReply.bind(this);
|
||||||
this.handleEditMessage = this.handleEditMessage.bind(this);
|
this.handleEditMessage = this.handleEditMessage.bind(this);
|
||||||
@@ -680,7 +689,10 @@ class MessagesView extends React.Component {
|
|||||||
if (this.state.imagePreview && this.state.imagePreview.url) {
|
if (this.state.imagePreview && this.state.imagePreview.url) {
|
||||||
URL.revokeObjectURL(this.state.imagePreview.url);
|
URL.revokeObjectURL(this.state.imagePreview.url);
|
||||||
}
|
}
|
||||||
this.setState({ imagePostview: null, imagePreview: null, docPreview: null });
|
if (this.state.videoPreview && this.state.videoPreview.url) {
|
||||||
|
URL.revokeObjectURL(this.state.videoPreview.url);
|
||||||
|
}
|
||||||
|
this.setState({ imagePostview: null, imagePreview: null, docPreview: null, videoPreview: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFormResponse(action, text, data) {
|
handleFormResponse(action, text, data) {
|
||||||
@@ -743,10 +755,14 @@ class MessagesView extends React.Component {
|
|||||||
this.props.onChangePermissions(this.state.topic, DEFAULT_P2P_ACCESS_MODE, this.state.topic);
|
this.props.onChangePermissions(this.state.topic, DEFAULT_P2P_ACCESS_MODE, this.state.topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendKeyPress() {
|
sendKeyPress(audio) {
|
||||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||||
if (topic.isSubscribed()) {
|
if (topic.isSubscribed()) {
|
||||||
topic.noteKeyPress();
|
if (audio) {
|
||||||
|
topic.noteRecording(true);
|
||||||
|
} else {
|
||||||
|
topic.noteKeyPress();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,7 +835,7 @@ class MessagesView extends React.Component {
|
|||||||
// Small enough to send inband.
|
// Small enough to send inband.
|
||||||
fileToBase64(file)
|
fileToBase64(file)
|
||||||
.then(b64 => this.sendMessage(Drafty.attachFile(null, {mime: b64.mime, data: b64.bits, filename: b64.name})))
|
.then(b64 => this.sendMessage(Drafty.attachFile(null, {mime: b64.mime, data: b64.bits, filename: b64.name})))
|
||||||
.catch(err => this.props.onError(err));
|
.catch(err => this.props.onError(err.message, 'err'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -851,7 +867,7 @@ class MessagesView extends React.Component {
|
|||||||
this.props.onCallHangup(topic, seq);
|
this.props.onCallHangup(topic, seq);
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendImageAttachment sends the image bits inband as Drafty message.
|
// sendImageAttachment sends the image bits as Drafty message.
|
||||||
sendImageAttachment(caption, blob) {
|
sendImageAttachment(caption, blob) {
|
||||||
const mime = this.state.imagePreview.mime;
|
const mime = this.state.imagePreview.mime;
|
||||||
const width = this.state.imagePreview.width;
|
const width = this.state.imagePreview.width;
|
||||||
@@ -876,32 +892,34 @@ class MessagesView extends React.Component {
|
|||||||
// Convert tiny image into base64 for serialization and previewing.
|
// Convert tiny image into base64 for serialization and previewing.
|
||||||
.then(scaled => blobToBase64(scaled.blob))
|
.then(scaled => blobToBase64(scaled.blob))
|
||||||
.then(b64 => {
|
.then(b64 => {
|
||||||
let msg = Drafty.insertImage(null, 0, {
|
let msg = Drafty.insertImage(null, 0, {
|
||||||
mime: mime,
|
mime: mime,
|
||||||
_tempPreview: b64.bits, // This preview will not be serialized.
|
_tempPreview: b64.bits, // This preview will not be serialized.
|
||||||
width: width,
|
bits: b64.bits, // Image thumbnail.
|
||||||
height: height,
|
width: width,
|
||||||
filename: fname,
|
height: height,
|
||||||
size: blob.size,
|
filename: fname,
|
||||||
urlPromise: uploadCompletionPromise
|
size: blob.size,
|
||||||
});
|
urlPromise: uploadCompletionPromise
|
||||||
if (caption) {
|
});
|
||||||
msg = Drafty.appendLineBreak(msg);
|
if (caption) {
|
||||||
msg = Drafty.append(msg, Drafty.parse(caption));
|
msg = Drafty.appendLineBreak(msg);
|
||||||
}
|
msg = Drafty.append(msg, Drafty.parse(caption));
|
||||||
// Pass data and the uploader to the TinodeWeb.
|
}
|
||||||
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
// Pass data and the uploader to the TinodeWeb.
|
||||||
|
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||||
})
|
})
|
||||||
.catch(err => this.props.onError(err, 'err'));
|
.catch(err => this.props.onError(err, 'err'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload the image if it's too big to be send inband.
|
// Send the image inband if it's not too big. The image has been scaled already
|
||||||
|
// in image preview.
|
||||||
blobToBase64(blob)
|
blobToBase64(blob)
|
||||||
.then(b64 => {
|
.then(b64 => {
|
||||||
let msg = Drafty.insertImage(null, 0, {
|
let msg = Drafty.insertImage(null, 0, {
|
||||||
mime: b64.mime,
|
mime: b64.mime,
|
||||||
preview: b64.bits, // Serializable preview
|
bits: b64.bits,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
filename: fname,
|
filename: fname,
|
||||||
@@ -915,10 +933,107 @@ class MessagesView extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAttachImage method is called when [Attach image] button is clicked: launch image preview.
|
// sendVideoAttachment sends the video bits as Drafty message.
|
||||||
handleAttachImage(file) {
|
sendVideoAttachment(caption, videoBlob, previewBlob, params) {
|
||||||
|
const width = params.width;
|
||||||
|
const height = params.height;
|
||||||
|
|
||||||
|
// Server-provided limit reduced for base64 encoding and overhead.
|
||||||
|
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
|
||||||
|
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
|
||||||
|
|
||||||
|
const uploads = [];
|
||||||
|
let uploader;
|
||||||
|
if ((videoBlob.size + previewBlob.size) > maxInbandAttachmentSize) {
|
||||||
|
// One or both are too large to send inband. Uploading out of band and sending as a link.
|
||||||
|
uploader = this.props.tinode.getLargeFileHelper();
|
||||||
|
if (!uploader) {
|
||||||
|
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploads[0] = videoBlob.size > maxInbandAttachmentSize * 0.675 ? uploader.upload(videoBlob) : null;
|
||||||
|
uploads[1] = previewBlob.size > maxInbandAttachmentSize * 0.275 ? uploader.upload(previewBlob) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploads.length == 0) {
|
||||||
|
// Both video and preview are small enough to send inband.
|
||||||
|
Promise.all(blobToBase64(videoBlob), blobToBase64(previewBlob))
|
||||||
|
.then(b64s => {
|
||||||
|
const [v64, i64] = b64s;
|
||||||
|
let msg = Drafty.insertVideo(null, 0, {
|
||||||
|
mime: v64.mime,
|
||||||
|
bits: v64.bits,
|
||||||
|
preview: i64.bits,
|
||||||
|
premime: i64.mime,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
duration: params.duration,
|
||||||
|
filename: params.name,
|
||||||
|
size: videoBlob.size
|
||||||
|
});
|
||||||
|
if (caption) {
|
||||||
|
msg = Drafty.appendLineBreak(msg);
|
||||||
|
msg = Drafty.append(msg, Drafty.parse(caption));
|
||||||
|
}
|
||||||
|
this.sendMessage(msg);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadCompletionPromise = Promise.all(uploads);
|
||||||
|
|
||||||
|
const b64conv = [];
|
||||||
|
// Small video converted to base64.
|
||||||
|
b64conv[0] = uploads[0] ? null : blobToBase64(videoBlob);
|
||||||
|
// Full-size preview fits inline.
|
||||||
|
b64conv[1] = uploads[1] ? null : imageScaled(previewBlob, MAX_IMAGE_DIM, MAX_IMAGE_DIM, -1, false)
|
||||||
|
.then(scaled => blobToBase64(scaled.blob));
|
||||||
|
// Small preview to show while uploading.
|
||||||
|
b64conv[2] = imageScaled(previewBlob, VIDEO_PREVIEW_DIM, VIDEO_PREVIEW_DIM, -1, false)
|
||||||
|
.then(scaled => blobToBase64(scaled.blob));
|
||||||
|
Promise.all(b64conv)
|
||||||
|
// Convert tiny image into base64 for serialization and previewing.
|
||||||
|
.then(b64s => {
|
||||||
|
const [video, img, preview] = b64s;
|
||||||
|
let msg = Drafty.insertVideo(null, 0, {
|
||||||
|
mime: params.mime,
|
||||||
|
bits: video ? video.bits : null,
|
||||||
|
_tempPreview: preview.bits,
|
||||||
|
preview: img ? img.bits : preview.bits,
|
||||||
|
premime: img ? img.mime : preview.mime,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
duration: params.duration,
|
||||||
|
filename: params.name,
|
||||||
|
size: videoBlob.size,
|
||||||
|
urlPromise: uploadCompletionPromise
|
||||||
|
});
|
||||||
|
if (caption) {
|
||||||
|
msg = Drafty.appendLineBreak(msg);
|
||||||
|
msg = Drafty.append(msg, Drafty.parse(caption));
|
||||||
|
}
|
||||||
|
// Pass data and the uploader to the TinodeWeb.
|
||||||
|
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||||
|
})
|
||||||
|
.catch(err => this.props.onError(err.message, 'err'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAttachImageOrVideo method is called when [Attach image or video] button is clicked: launch image or video preview.
|
||||||
|
handleAttachImageOrVideo(file) {
|
||||||
const maxExternAttachmentSize = this.props.tinode.getServerParam('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
|
const maxExternAttachmentSize = this.props.tinode.getServerParam('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
|
||||||
|
|
||||||
|
if (file.type.startsWith('video/')) {
|
||||||
|
this.setState({videoPreview: {
|
||||||
|
url: URL.createObjectURL(file),
|
||||||
|
blob: file,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
mime: file.type
|
||||||
|
}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get image dimensions and size, optionally scale it down.
|
// Get image dimensions and size, optionally scale it down.
|
||||||
imageScaled(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, maxExternAttachmentSize, false)
|
imageScaled(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, maxExternAttachmentSize, false)
|
||||||
.then(scaled => {
|
.then(scaled => {
|
||||||
@@ -932,10 +1047,23 @@ class MessagesView extends React.Component {
|
|||||||
mime: scaled.mime
|
mime: scaled.mime
|
||||||
}});
|
}});
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
this.props.onError(err, 'err');
|
this.props.onError(err.message, 'err');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleFileDrop is called when the user drags & drops a file upon the message view.
|
||||||
|
handleFileDrop(files) {
|
||||||
|
if (!files || files.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = files[0];
|
||||||
|
if (file.type && file.type.startsWith('image/')) {
|
||||||
|
this.handleAttachImageOrVideo(file);
|
||||||
|
} else {
|
||||||
|
this.handleAttachFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sendAudioAttachment sends audio bits inband as Drafty message (no preview).
|
// sendAudioAttachment sends audio bits inband as Drafty message (no preview).
|
||||||
sendAudioAttachment(url, preview, duration) {
|
sendAudioAttachment(url, preview, duration) {
|
||||||
fetch(url)
|
fetch(url)
|
||||||
@@ -966,15 +1094,15 @@ class MessagesView extends React.Component {
|
|||||||
.then(b64 => {
|
.then(b64 => {
|
||||||
this.sendMessage(Drafty.appendAudio(null, {
|
this.sendMessage(Drafty.appendAudio(null, {
|
||||||
mime: b64.mime,
|
mime: b64.mime,
|
||||||
|
bits: b64.bits,
|
||||||
size: blob.size,
|
size: blob.size,
|
||||||
data: b64.bits,
|
|
||||||
duration: duration,
|
duration: duration,
|
||||||
preview: preview,
|
preview: preview,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {this.props.onError(err)});;
|
.catch(err => {this.props.onError(err.message, 'err')});;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancelUpload(seq, uploader) {
|
handleCancelUpload(seq, uploader) {
|
||||||
@@ -1085,6 +1213,17 @@ class MessagesView extends React.Component {
|
|||||||
onClose={this.handleClosePreview}
|
onClose={this.handleClosePreview}
|
||||||
onSendMessage={this.sendImageAttachment} />
|
onSendMessage={this.sendImageAttachment} />
|
||||||
);
|
);
|
||||||
|
} else if (this.state.videoPreview) {
|
||||||
|
// Preview video.
|
||||||
|
component2 = (
|
||||||
|
<VideoPreview
|
||||||
|
content={this.state.videoPreview}
|
||||||
|
tinode={this.props.tinode}
|
||||||
|
reply={this.state.reply}
|
||||||
|
onCancelReply={this.handleCancelReply}
|
||||||
|
onClose={this.handleClosePreview}
|
||||||
|
onSendMessage={this.sendVideoAttachment} />
|
||||||
|
);
|
||||||
} else if (this.state.imagePostview) {
|
} else if (this.state.imagePostview) {
|
||||||
// Expand received image.
|
// Expand received image.
|
||||||
component2 = (
|
component2 = (
|
||||||
@@ -1141,7 +1280,6 @@ class MessagesView extends React.Component {
|
|||||||
let previousFrom = null;
|
let previousFrom = null;
|
||||||
let prevDate = null;
|
let prevDate = null;
|
||||||
let chatBoxClass = null;
|
let chatBoxClass = null;
|
||||||
const dateFmt = new Intl.DateTimeFormat(this.props.intl.locale);
|
|
||||||
topic.messages((msg, prev, next, i) => {
|
topic.messages((msg, prev, next, i) => {
|
||||||
let nextFrom = next ? (next.from || 'chan') : null;
|
let nextFrom = next ? (next.from || 'chan') : null;
|
||||||
|
|
||||||
@@ -1189,7 +1327,7 @@ class MessagesView extends React.Component {
|
|||||||
if (!prevDate || prevDate.toDateString() != thisDate.toDateString()) {
|
if (!prevDate || prevDate.toDateString() != thisDate.toDateString()) {
|
||||||
messageNodes.push(
|
messageNodes.push(
|
||||||
<MetaMessage
|
<MetaMessage
|
||||||
date={dateFmt.format(msg.ts)}
|
date={relativeDateFormat(msg.ts)}
|
||||||
locale={this.props.intl.locale}
|
locale={this.props.intl.locale}
|
||||||
key={'date-' + msg.seq} />
|
key={'date-' + msg.seq} />
|
||||||
);
|
);
|
||||||
@@ -1250,6 +1388,59 @@ class MessagesView extends React.Component {
|
|||||||
|
|
||||||
const titleClass = 'panel-title' + (this.state.deleted ? ' deleted' : '');
|
const titleClass = 'panel-title' + (this.state.deleted ? ' deleted' : '');
|
||||||
|
|
||||||
|
let messagesComponent = (
|
||||||
|
<>
|
||||||
|
<div id="messages-container">
|
||||||
|
<button className={'action-button' + (this.state.showGoToLastButton ? '' : ' hidden')}
|
||||||
|
onClick={this.goToLatestMessage}>
|
||||||
|
<i className="material-icons">arrow_downward</i>
|
||||||
|
</button>
|
||||||
|
<div id="messages-panel" ref={this.handleScrollReference}>
|
||||||
|
<ul id="scroller" className={chatBoxClass}>
|
||||||
|
{messageNodes}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{!this.state.isReader ?
|
||||||
|
<div id="write-only-background">
|
||||||
|
{this.state.readingBlocked ?
|
||||||
|
<div id="write-only-note">
|
||||||
|
<FormattedMessage id="messages_not_readable" defaultMessage="no access to messages"
|
||||||
|
description="Message shown in topic without the read access" />
|
||||||
|
</div>
|
||||||
|
: null }
|
||||||
|
</div>
|
||||||
|
: null }
|
||||||
|
</div>
|
||||||
|
{this.state.peerMessagingDisabled && !this.state.unconfirmed ?
|
||||||
|
<div id="peer-messaging-disabled-note">
|
||||||
|
<i className="material-icons secondary">block</i> <FormattedMessage
|
||||||
|
id="peers_messaging_disabled" defaultMessage="Peer's messaging is disabled."
|
||||||
|
description="Shown when the p2p peer's messaging is disabled" /> <a href="#"
|
||||||
|
onClick={this.handleEnablePeer}><FormattedMessage id="enable_peers_messaging"
|
||||||
|
defaultMessage="Enable" description="Call to action to enable peer's messaging" /></a>.
|
||||||
|
</div> : null}
|
||||||
|
{this.state.unconfirmed ?
|
||||||
|
<Invitation onAction={this.handleNewChatAcceptance} />
|
||||||
|
:
|
||||||
|
<SendMessage
|
||||||
|
tinode={this.props.tinode}
|
||||||
|
topicName={this.state.topic}
|
||||||
|
noInput={!!this.props.forwardMessage}
|
||||||
|
disabled={!this.state.isWriter || this.state.deleted}
|
||||||
|
reply={this.state.reply}
|
||||||
|
initMessage={this.state.contentToEdit}
|
||||||
|
onKeyPress={this.sendKeyPress}
|
||||||
|
onRecordingProgress={this.sendKeyPress}
|
||||||
|
onSendMessage={this.sendMessage}
|
||||||
|
onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile}
|
||||||
|
onAttachImage={this.props.forwardMessage ? null : this.handleAttachImageOrVideo}
|
||||||
|
onAttachAudio={this.props.forwardMessage ? null : this.sendAudioAttachment}
|
||||||
|
onError={this.props.onError}
|
||||||
|
onQuoteClick={this.handleQuoteClick}
|
||||||
|
onCancelReply={this.handleCancelReply} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
component2 = (
|
component2 = (
|
||||||
<>
|
<>
|
||||||
<div id="topic-caption-panel" className="caption-panel">
|
<div id="topic-caption-panel" className="caption-panel">
|
||||||
@@ -1295,53 +1486,9 @@ class MessagesView extends React.Component {
|
|||||||
onClearError={this.props.onError} />
|
onClearError={this.props.onError} />
|
||||||
: null}
|
: null}
|
||||||
<LoadSpinner show={this.state.fetchingMessages} />
|
<LoadSpinner show={this.state.fetchingMessages} />
|
||||||
<div id="messages-container">
|
{!this.state.unconfirmed && !this.props.forwardMessage ?
|
||||||
<button className={'action-button' + (this.state.showGoToLastButton ? '' : ' hidden')}
|
<DragAndDrop actionPrompt={this.props.intl.formatMessage(messages.drag_file)} onDrop={this.handleFileDrop} >{messagesComponent}</DragAndDrop>
|
||||||
onClick={this.goToLatestMessage}>
|
: messagesComponent}
|
||||||
<i className="material-icons">arrow_downward</i>
|
|
||||||
</button>
|
|
||||||
<div id="messages-panel" ref={this.handleScrollReference}>
|
|
||||||
<ul id="scroller" className={chatBoxClass}>
|
|
||||||
{messageNodes}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{!this.state.isReader ?
|
|
||||||
<div id="write-only-background">
|
|
||||||
{this.state.readingBlocked ?
|
|
||||||
<div id="write-only-note">
|
|
||||||
<FormattedMessage id="messages_not_readable" defaultMessage="no access to messages"
|
|
||||||
description="Message shown in topic without the read access" />
|
|
||||||
</div>
|
|
||||||
: null }
|
|
||||||
</div>
|
|
||||||
: null }
|
|
||||||
</div>
|
|
||||||
{this.state.peerMessagingDisabled && !this.state.unconfirmed ?
|
|
||||||
<div id="peer-messaging-disabled-note">
|
|
||||||
<i className="material-icons secondary">block</i> <FormattedMessage
|
|
||||||
id="peers_messaging_disabled" defaultMessage="Peer's messaging is disabled."
|
|
||||||
description="Shown when the p2p peer's messaging is disabled" /> <a href="#"
|
|
||||||
onClick={this.handleEnablePeer}><FormattedMessage id="enable_peers_messaging"
|
|
||||||
defaultMessage="Enable" description="Call to action to enable peer's messaging" /></a>.
|
|
||||||
</div> : null}
|
|
||||||
{this.state.unconfirmed ?
|
|
||||||
<Invitation onAction={this.handleNewChatAcceptance} />
|
|
||||||
:
|
|
||||||
<SendMessage
|
|
||||||
tinode={this.props.tinode}
|
|
||||||
topicName={this.state.topic}
|
|
||||||
noInput={!!this.props.forwardMessage}
|
|
||||||
disabled={!this.state.isWriter || this.state.deleted}
|
|
||||||
reply={this.state.reply}
|
|
||||||
initMessage={this.state.contentToEdit}
|
|
||||||
onKeyPress={this.sendKeyPress}
|
|
||||||
onSendMessage={this.sendMessage}
|
|
||||||
onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile}
|
|
||||||
onAttachImage={this.props.forwardMessage ? null : this.handleAttachImage}
|
|
||||||
onAttachAudio={this.props.forwardMessage ? null : this.sendAudioAttachment}
|
|
||||||
onError={this.props.onError}
|
|
||||||
onQuoteClick={this.handleQuoteClick}
|
|
||||||
onCancelReply={this.handleCancelReply} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleCallIceCandidate = this.handleCallIceCandidate.bind(this);
|
this.handleCallIceCandidate = this.handleCallIceCandidate.bind(this);
|
||||||
this.handleCallSendAnswer = this.handleCallSendAnswer.bind(this);
|
this.handleCallSendAnswer = this.handleCallSendAnswer.bind(this);
|
||||||
|
|
||||||
this.handleCallAcceptCall = this.handleCallAcceptCall.bind(this);
|
this.handleCallAccept = this.handleCallAccept.bind(this);
|
||||||
|
|
||||||
this.sendMessageToTopic = this.sendMessageToTopic.bind(this);
|
this.sendMessageToTopic = this.sendMessageToTopic.bind(this);
|
||||||
|
|
||||||
@@ -1791,7 +1791,7 @@ class TinodeWeb extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCallAcceptCall(topicName) {
|
handleCallAccept(topicName) {
|
||||||
const topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return;
|
return;
|
||||||
@@ -1914,7 +1914,7 @@ class TinodeWeb extends React.Component {
|
|||||||
seq={this.state.callSeq}
|
seq={this.state.callSeq}
|
||||||
callState={this.state.callState}
|
callState={this.state.callState}
|
||||||
onRinging={this.handleCallRinging}
|
onRinging={this.handleCallRinging}
|
||||||
onAcceptCall={this.handleCallAcceptCall}
|
onAcceptCall={this.handleCallAccept}
|
||||||
onReject={this.handleCallHangup}
|
onReject={this.handleCallHangup}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Audio recorder widget.
|
// Audio recorder widget.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { secondsToTime } from '../lib/strformat';
|
import { secondsToTime } from '../lib/strformat';
|
||||||
import { base64ToIntArray } from '../lib/blob-helpers';
|
import { base64ToIntArray } from '../lib/blob-helpers';
|
||||||
@@ -18,7 +19,15 @@ const THUMB_COLOR = '#444E';
|
|||||||
// Minimum number of amplitude bars to draw.
|
// Minimum number of amplitude bars to draw.
|
||||||
const MIN_PREVIEW_LENGTH = 16;
|
const MIN_PREVIEW_LENGTH = 16;
|
||||||
|
|
||||||
export default class AudioPlayer extends React.PureComponent {
|
const messages = defineMessages({
|
||||||
|
icon_title_play: {
|
||||||
|
id: 'icon_title_play',
|
||||||
|
defaultMessage: 'Play recording',
|
||||||
|
description: 'Icon tool tip for starting audio playback'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class AudioPlayer extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -237,7 +246,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
const playClass = 'material-icons' +
|
const playClass = 'material-icons' +
|
||||||
(this.props.short ? '' : ' large') +
|
(this.props.short ? '' : ' large') +
|
||||||
(this.state.canPlay ? '' : ' disabled');
|
(this.state.canPlay ? '' : ' disabled');
|
||||||
const play = (<a href="#" onClick={this.handlePlay} title="Play">
|
const play = (<a href="#" onClick={this.handlePlay} title={this.props.intl.formatMessage(messages.icon_title_play)}>
|
||||||
<i className={playClass}>{this.state.playing ? 'pause_circle' :
|
<i className={playClass}>{this.state.playing ? 'pause_circle' :
|
||||||
(this.state.canPlay ? 'play_circle' : 'not_interested')}</i>
|
(this.state.canPlay ? 'play_circle' : 'not_interested')}</i>
|
||||||
</a>);
|
</a>);
|
||||||
@@ -258,3 +267,5 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default injectIntl(AudioPlayer);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Audio recorder widget.
|
// Audio recorder widget.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import AudioPlayer from './audio-player.jsx';
|
import AudioPlayer from './audio-player.jsx';
|
||||||
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=642012
|
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=642012
|
||||||
@@ -9,7 +10,7 @@ import fixWebmDuration from 'webm-duration-fix';
|
|||||||
|
|
||||||
import { intArrayToBase64 } from '../lib/blob-helpers.js'
|
import { intArrayToBase64 } from '../lib/blob-helpers.js'
|
||||||
import { secondsToTime } from '../lib/strformat';
|
import { secondsToTime } from '../lib/strformat';
|
||||||
import { MAX_DURATION, MIN_DURATION } from '../config.js';
|
import { KEYPRESS_DELAY, MAX_DURATION, MIN_DURATION } from '../config.js';
|
||||||
|
|
||||||
// FFT resolution.
|
// FFT resolution.
|
||||||
const BUFFER_SIZE = 256;
|
const BUFFER_SIZE = 256;
|
||||||
@@ -33,7 +34,30 @@ const MAX_SAMPLES_PER_BAR = 10;
|
|||||||
// Recording format.
|
// Recording format.
|
||||||
const AUDIO_MIME_TYPE = 'audio/webm';
|
const AUDIO_MIME_TYPE = 'audio/webm';
|
||||||
|
|
||||||
export default class AudioRecorder extends React.PureComponent {
|
const messages = defineMessages({
|
||||||
|
icon_title_delete: {
|
||||||
|
id: 'icon_title_delete',
|
||||||
|
defaultMessage: 'Delete recording',
|
||||||
|
description: 'Icon tool tip for deleting recorded audio'
|
||||||
|
},
|
||||||
|
icon_title_pause: {
|
||||||
|
id: 'icon_title_pause',
|
||||||
|
defaultMessage: 'Pause playback',
|
||||||
|
description: 'Icon tool tip for pausing audio playback'
|
||||||
|
},
|
||||||
|
icon_title_resume: {
|
||||||
|
id: 'icon_title_resume',
|
||||||
|
defaultMessage: 'Resume playback',
|
||||||
|
description: 'Icon tool tip for resuming audio playback'
|
||||||
|
},
|
||||||
|
icon_title_send: {
|
||||||
|
id: 'icon_title_send',
|
||||||
|
defaultMessage: 'Send message',
|
||||||
|
description: 'Icon tool tip for sending a message'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class AudioRecorder extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -62,6 +86,9 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
this.startedOn = null;
|
this.startedOn = null;
|
||||||
this.viewBuffer = [];
|
this.viewBuffer = [];
|
||||||
this.canvasRef = React.createRef();
|
this.canvasRef = React.createRef();
|
||||||
|
|
||||||
|
// Timestamp for sending "recording" notifications.
|
||||||
|
this.recordingTimestamp = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -168,6 +195,13 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
// Actually draw the bars on canvas.
|
// Actually draw the bars on canvas.
|
||||||
this.canvasContext.stroke();
|
this.canvasContext.stroke();
|
||||||
|
|
||||||
|
// Send notification, if needed.
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (now - this.recordingTimestamp > KEYPRESS_DELAY) {
|
||||||
|
this.props.onRecordingProgress();
|
||||||
|
this.recordingTimestamp = now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawFrame();
|
drawFrame();
|
||||||
@@ -238,7 +272,7 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
|
|
||||||
this.mediaRecorder.onstop = _ => {
|
this.mediaRecorder.onstop = _ => {
|
||||||
if (this.durationMillis > MIN_DURATION) {
|
if (this.durationMillis > MIN_DURATION) {
|
||||||
this.getRecording(this.mediaRecorder.mimeType, this.durationMillis)
|
this.getRecording(this.mediaRecorder.mimeType)
|
||||||
.then(result => this.props.onFinished(result.url, result.preview, this.durationMillis));
|
.then(result => this.props.onFinished(result.url, result.preview, this.durationMillis));
|
||||||
} else {
|
} else {
|
||||||
this.props.onDeleted();
|
this.props.onDeleted();
|
||||||
@@ -265,11 +299,14 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
this.startedOn = Date.now();
|
this.startedOn = Date.now();
|
||||||
this.mediaRecorder.start();
|
this.mediaRecorder.start();
|
||||||
this.visualize();
|
this.visualize();
|
||||||
|
|
||||||
|
this.props.onRecordingProgress();
|
||||||
|
this.recordingTimestamp = this.startedOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain data in a form sutable for sending or playing back.
|
// Obtain data in a form sutable for sending or playing back.
|
||||||
// If duration is valid, apply fix for Chrome's WebM duration bug.
|
// If duration is valid, apply fix for Chrome's WebM duration bug.
|
||||||
getRecording(mimeType, duration) {
|
getRecording(mimeType) {
|
||||||
mimeType = mimeType || AUDIO_MIME_TYPE;
|
mimeType = mimeType || AUDIO_MIME_TYPE;
|
||||||
let blob = new Blob(this.audioChunks, {type: mimeType});
|
let blob = new Blob(this.audioChunks, {type: mimeType});
|
||||||
return fixWebmDuration(blob, mimeType)
|
return fixWebmDuration(blob, mimeType)
|
||||||
@@ -315,10 +352,11 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
const resumeClass = 'material-icons ' + (this.state.enabled ? 'red' : 'gray');
|
const resumeClass = 'material-icons ' + (this.state.enabled ? 'red' : 'gray');
|
||||||
return (
|
return (
|
||||||
<div className="audio">
|
<div className="audio">
|
||||||
<a href="#" onClick={this.handleDelete} title="Delete">
|
<a href="#" onClick={this.handleDelete} title={formatMessage(messages.icon_title_delete)}>
|
||||||
<i className="material-icons gray">delete_outline</i>
|
<i className="material-icons gray">delete_outline</i>
|
||||||
</a>
|
</a>
|
||||||
{this.state.recording ?
|
{this.state.recording ?
|
||||||
@@ -332,17 +370,19 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
<div className="duration">{this.state.duration}</div>
|
<div className="duration">{this.state.duration}</div>
|
||||||
{this.state.recording ?
|
{this.state.recording ?
|
||||||
<a href="#" onClick={this.handlePause} title="Pause">
|
<a href="#" onClick={this.handlePause} title={formatMessage(messages.icon_title_pause)}>
|
||||||
<i className="material-icons">pause_circle_outline</i>
|
<i className="material-icons">pause_circle_outline</i>
|
||||||
</a> :
|
</a> :
|
||||||
<a href="#" onClick={this.handleResume} title="Resume">
|
<a href="#" onClick={this.handleResume} title={formatMessage(messages.icon_title_resume)}>
|
||||||
<i className={resumeClass}>radio_button_checked</i>
|
<i className={resumeClass}>radio_button_checked</i>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
<a href="#" onClick={this.handleDone} title="Send">
|
<a href="#" onClick={this.handleDone} title={formatMessage(messages.icon_title_send)}>
|
||||||
<i className="material-icons">send</i>
|
<i className="material-icons">send</i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default injectIntl(AudioRecorder);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default class CallIncoming extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resetDesc(topic, this.props);
|
this.resetDesc(topic);
|
||||||
if (this.props.callState == CALL_STATE_INCOMING_RECEIVED) {
|
if (this.props.callState == CALL_STATE_INCOMING_RECEIVED) {
|
||||||
// play() throws if the user did not click the app first: https://goo.gl/xX8pDD.
|
// play() throws if the user did not click the app first: https://goo.gl/xX8pDD.
|
||||||
RING_SOUND.play().catch(_ => {});
|
RING_SOUND.play().catch(_ => {});
|
||||||
@@ -77,9 +77,7 @@ export default class CallIncoming extends React.Component {
|
|||||||
topic.onMetaDesc = this.previousMetaDesc;
|
topic.onMetaDesc = this.previousMetaDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetDesc(topic, props) {
|
resetDesc(topic) {
|
||||||
const defacs = topic.getDefaultAccess() || {};
|
|
||||||
const acs = topic.getAccessMode();
|
|
||||||
const badges = [];
|
const badges = [];
|
||||||
if (topic.trusted) {
|
if (topic.trusted) {
|
||||||
for (const [key, val] of Object.entries(topic.trusted)) {
|
for (const [key, val] of Object.entries(topic.trusted)) {
|
||||||
@@ -101,7 +99,7 @@ export default class CallIncoming extends React.Component {
|
|||||||
if (!topic) {
|
if (!topic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.resetDesc(topic, this.props);
|
this.resetDesc(topic);
|
||||||
|
|
||||||
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
|
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
|
||||||
this.previousMetaDesc(desc);
|
this.previousMetaDesc(desc);
|
||||||
|
|||||||
@@ -421,14 +421,14 @@ class CallPanel extends React.PureComponent {
|
|||||||
<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">
|
||||||
<video ref={this.localRef} autoPlay muted playsInline></video>
|
<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">
|
||||||
<video ref={this.remoteRef} autoPlay playsInline></video>
|
<video ref={this.remoteRef} autoPlay playsInline />
|
||||||
{remoteActive ?
|
{remoteActive ?
|
||||||
<div className="caller-name inactive">{peerTitle}</div> :
|
<div className="caller-name inactive">{peerTitle}</div> :
|
||||||
<div className={`caller-card${pulseAnimation}`}>
|
<div className={`caller-card${pulseAnimation}`}>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
|
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
|
||||||
|
|
||||||
onImagePreview: this.handleImagePreview,
|
onImagePreview: this.handleImagePreview,
|
||||||
|
onVideoPreview: this.handleImagePreview,
|
||||||
onFormButtonClick: this.handleFormButtonClick,
|
onFormButtonClick: this.handleFormButtonClick,
|
||||||
onQuoteClick: this.handleQuoteClick
|
onQuoteClick: this.handleQuoteClick
|
||||||
};
|
};
|
||||||
@@ -76,12 +77,17 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
|
if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
|
||||||
menuItems.push('menu_item_send_retry');
|
menuItems.push('menu_item_send_retry');
|
||||||
}
|
}
|
||||||
if (this.props.userIsWriter &&
|
if (this.props.userIsWriter && this.props.received > Tinode.MESSAGE_STATUS_FAILED) {
|
||||||
this.props.received > Tinode.MESSAGE_STATUS_FAILED &&
|
|
||||||
this.props.received < Tinode.MESSAGE_STATUS_DEL_RANGE) {
|
|
||||||
menuItems.push('menu_item_reply');
|
menuItems.push('menu_item_reply');
|
||||||
if (!this.props.response) {
|
if (!this.props.response) {
|
||||||
menuItems.push('menu_item_edit');
|
let immutable = false;
|
||||||
|
Drafty.entities(this.props.content, (_0, _1, tp) => {
|
||||||
|
immutable = ['AU', 'EX', 'FM', 'IM', 'QQ', 'VC', 'VD'].includes(tp);
|
||||||
|
return immutable;
|
||||||
|
});
|
||||||
|
if (!immutable) {
|
||||||
|
menuItems.push('menu_item_edit');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
menuItems.push('menu_item_forward');
|
menuItems.push('menu_item_forward');
|
||||||
@@ -146,12 +152,6 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
}, this);
|
}, this);
|
||||||
const tree = Drafty.format(content, fullFormatter, this.formatterContext);
|
const tree = Drafty.format(content, fullFormatter, this.formatterContext);
|
||||||
content = React.createElement(React.Fragment, null, tree);
|
content = React.createElement(React.Fragment, null, tree);
|
||||||
} else if (this.props.deleted) {
|
|
||||||
// Message represents a range of deleted messages.
|
|
||||||
content = <><i className="material-icons gray">block</i> <i className="gray">
|
|
||||||
<FormattedMessage id="deleted_content"
|
|
||||||
defaultMessage="content deleted" description="Shown when messages are deleted" />
|
|
||||||
</i></>
|
|
||||||
} else if (typeof content != 'string') {
|
} else if (typeof content != 'string') {
|
||||||
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
|
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
|
||||||
<FormattedMessage id="invalid_content"
|
<FormattedMessage id="invalid_content"
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class ContactList extends React.Component {
|
|||||||
let previewIsResponse;
|
let previewIsResponse;
|
||||||
let deliveryStatus;
|
let deliveryStatus;
|
||||||
if (!this.props.showMode && c.latestMessage) {
|
if (!this.props.showMode && c.latestMessage) {
|
||||||
const msg = c.latestMessage(true);
|
const msg = c.latestMessage();
|
||||||
if (msg) {
|
if (msg) {
|
||||||
forwarded = msg.head ? msg.head.forwarded : null;
|
forwarded = msg.head ? msg.head.forwarded : null;
|
||||||
deliveryStatus = msg._status || c.msgStatus(msg, true);
|
deliveryStatus = msg._status || c.msgStatus(msg, true);
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Drag & drop container: displays a banner on drag and calls a handler on drop.
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default class DragAndDrop extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
// A file is being dragged over the container.
|
||||||
|
dragging: false
|
||||||
|
};
|
||||||
|
this.dropRef = React.createRef();
|
||||||
|
// Keeps track of the drag event.
|
||||||
|
// Need a counter b/c the browser's 'drag' events may fire multiple times
|
||||||
|
// when the user takes the mouse pointer over the container:
|
||||||
|
// for the component itself and for all nested/child elements.
|
||||||
|
this.dragCounter = 0;
|
||||||
|
|
||||||
|
this.handleDrag = this.handleDrag.bind(this);
|
||||||
|
this.handleDragIn = this.handleDragIn.bind(this);
|
||||||
|
this.handleDragOut = this.handleDragOut.bind(this);
|
||||||
|
this.handleDrop = this.handleDrop.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let div = this.dropRef.current;
|
||||||
|
div.addEventListener('dragenter', this.handleDragIn);
|
||||||
|
div.addEventListener('dragleave', this.handleDragOut);
|
||||||
|
div.addEventListener('dragover', this.handleDrag);
|
||||||
|
div.addEventListener('drop', this.handleDrop);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
let div = this.dropRef.current;
|
||||||
|
div.removeEventListener('dragenter', this.handleDragIn);
|
||||||
|
div.removeEventListener('dragleave', this.handleDragOut);
|
||||||
|
div.removeEventListener('dragover', this.handleDrag);
|
||||||
|
div.removeEventListener('drop', this.handleDrop);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrag(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragIn(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.dragCounter++;
|
||||||
|
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
||||||
|
this.setState({dragging: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOut(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.dragCounter--;
|
||||||
|
if (this.dragCounter == 0) {
|
||||||
|
this.setState({dragging: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({dragging: false});
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||||
|
this.props.onDrop(e.dataTransfer.files);
|
||||||
|
e.dataTransfer.clearData();
|
||||||
|
this.dragCounter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='drag-and-drop' ref={this.dropRef}>
|
||||||
|
{this.state.dragging ?
|
||||||
|
<div className='banner'>{this.props.actionPrompt}</div>
|
||||||
|
: null}
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import SendMessage from '../widgets/send-message.jsx';
|
import SendMessage from './send-message.jsx';
|
||||||
|
|
||||||
import { REM_SIZE } from '../config.js';
|
import { REM_SIZE } from '../config.js';
|
||||||
import { fitImageSize } from '../lib/blob-helpers.js';
|
import { fitImageSize } from '../lib/blob-helpers.js';
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Element which shows static video preview:
|
||||||
|
// an image with duration and a play button in the middle.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { secondsToTime } from '../lib/strformat.js'
|
||||||
|
|
||||||
|
export default class InlineVideo extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const duration = secondsToTime(this.props['data-duration'] / 1000);
|
||||||
|
return (
|
||||||
|
<div className="inline-video">
|
||||||
|
{React.createElement('img', this.props)}
|
||||||
|
{this.props.onClick ?
|
||||||
|
<div className="play-control" onClick={this.props.onClick}>
|
||||||
|
<i className="material-icons white bigger">play_arrow</i>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
{duration ? <div className="duration">{duration}</div> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
// Message bubble with non-payload content, such as an indicator of deleted content or a date stamp.
|
// Message bubble with non-payload content, such as an indicator of deleted content or a date stamp.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
export default class MetaMessage extends React.PureComponent {
|
export default class MetaMessage extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -11,14 +10,7 @@ export default class MetaMessage extends React.PureComponent {
|
|||||||
render() {
|
render() {
|
||||||
let content = null;
|
let content = null;
|
||||||
let bubbleClass = 'bubble';
|
let bubbleClass = 'bubble';
|
||||||
if (this.props.deleted) {
|
if (this.props.date) {
|
||||||
// Message represents a range of deleted messages.
|
|
||||||
content = <><i className="material-icons gray">block</i> <i className="gray">
|
|
||||||
<FormattedMessage id="deleted_content"
|
|
||||||
defaultMessage="content deleted" description="Shown when messages are deleted" />
|
|
||||||
</i></>
|
|
||||||
bubbleClass += ' deleted';
|
|
||||||
} else if (this.props.date) {
|
|
||||||
// A date badge.
|
// A date badge.
|
||||||
content = <>{this.props.date}</>
|
content = <>{this.props.date}</>
|
||||||
bubbleClass += ' date';
|
bubbleClass += ' date';
|
||||||
|
|||||||
@@ -37,6 +37,26 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Cannot initiate file upload.',
|
defaultMessage: 'Cannot initiate file upload.',
|
||||||
description: 'Generic error messagewhen attachment fails'
|
description: 'Generic error messagewhen attachment fails'
|
||||||
},
|
},
|
||||||
|
icon_title_record_voice: {
|
||||||
|
id: 'icon_title_record_voice',
|
||||||
|
defaultMessage: 'Record voice message',
|
||||||
|
description: 'Icon tool tip for recording a voice message'
|
||||||
|
},
|
||||||
|
icon_title_attach_file: {
|
||||||
|
id: 'icon_title_attach_file',
|
||||||
|
defaultMessage: 'Attach file',
|
||||||
|
description: 'Icon tool tip for attaching a file'
|
||||||
|
},
|
||||||
|
icon_title_add_image: {
|
||||||
|
id: 'icon_title_add_image',
|
||||||
|
defaultMessage: 'Add image',
|
||||||
|
description: 'Icon tool tip for attaching an image'
|
||||||
|
},
|
||||||
|
icon_title_send: {
|
||||||
|
id: 'icon_title_send',
|
||||||
|
defaultMessage: 'Send message',
|
||||||
|
description: 'Icon tool tip for sending a message'
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
class SendMessage extends React.PureComponent {
|
class SendMessage extends React.PureComponent {
|
||||||
@@ -48,10 +68,11 @@ class SendMessage extends React.PureComponent {
|
|||||||
message: '',
|
message: '',
|
||||||
audioRec: false,
|
audioRec: false,
|
||||||
audioAvailable: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
audioAvailable: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
||||||
// Make initial keypress time as if it happened 5001 milliseconds in the past.
|
|
||||||
keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Timestamp when the previous key press was sent to the server.
|
||||||
|
this.keypressTimestamp = 0;
|
||||||
|
|
||||||
this.handlePasteEvent = this.handlePasteEvent.bind(this);
|
this.handlePasteEvent = this.handlePasteEvent.bind(this);
|
||||||
this.handleAttachImage = this.handleAttachImage.bind(this);
|
this.handleAttachImage = this.handleAttachImage.bind(this);
|
||||||
this.handleAttachFile = this.handleAttachFile.bind(this);
|
this.handleAttachFile = this.handleAttachFile.bind(this);
|
||||||
@@ -59,6 +80,7 @@ class SendMessage extends React.PureComponent {
|
|||||||
this.handleSend = this.handleSend.bind(this);
|
this.handleSend = this.handleSend.bind(this);
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||||
this.handleMessageTyping = this.handleMessageTyping.bind(this);
|
this.handleMessageTyping = this.handleMessageTyping.bind(this);
|
||||||
|
this.handleDropAttach = this.handleDropAttach.bind(this);
|
||||||
|
|
||||||
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
||||||
|
|
||||||
@@ -138,6 +160,13 @@ class SendMessage extends React.PureComponent {
|
|||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDropAttach(files) {
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
console.log('Dropping ', files);
|
||||||
|
this.props.onAttachFile(files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleAttachAudio(url, preview, duration) {
|
handleAttachAudio(url, preview, duration) {
|
||||||
this.setState({audioRec: false});
|
this.setState({audioRec: false});
|
||||||
this.props.onAttachAudio(url, preview, duration);
|
this.props.onAttachAudio(url, preview, duration);
|
||||||
@@ -174,15 +203,14 @@ class SendMessage extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMessageTyping(e) {
|
handleMessageTyping(e) {
|
||||||
const newState = {message: e.target.value};
|
this.setState({message: e.target.value});
|
||||||
if (this.props.onKeyPress) {
|
if (this.props.onKeyPress) {
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (now - this.state.keypressTimestamp > KEYPRESS_DELAY) {
|
if (now - this.keypressTimestamp > KEYPRESS_DELAY) {
|
||||||
this.props.onKeyPress();
|
this.props.onKeyPress();
|
||||||
newState.keypressTimestamp = now;
|
this.keypressTimestamp = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState(newState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleQuoteClick(e) {
|
handleQuoteClick(e) {
|
||||||
@@ -221,10 +249,10 @@ class SendMessage extends React.PureComponent {
|
|||||||
<>
|
<>
|
||||||
{this.props.onAttachFile && !this.state.audioRec ?
|
{this.props.onAttachFile && !this.state.audioRec ?
|
||||||
<>
|
<>
|
||||||
<a href="#" onClick={e => {e.preventDefault(); this.attachImage.click();}} title="Add image">
|
<a href="#" onClick={e => {e.preventDefault(); this.attachImage.click();}} title={formatMessage(messages.icon_title_add_image)}>
|
||||||
<i className="material-icons secondary">photo</i>
|
<i className="material-icons secondary">photo</i>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" onClick={e => {e.preventDefault(); this.attachFile.click();}} title="Attach file">
|
<a href="#" onClick={e => {e.preventDefault(); this.attachFile.click();}} title={formatMessage(messages.icon_title_attach_file)}>
|
||||||
<i className="material-icons secondary">attach_file</i>
|
<i className="material-icons secondary">attach_file</i>
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
@@ -235,28 +263,28 @@ class SendMessage extends React.PureComponent {
|
|||||||
(this.state.audioRec ?
|
(this.state.audioRec ?
|
||||||
(<Suspense fallback={<div>Loading...</div>}>
|
(<Suspense fallback={<div>Loading...</div>}>
|
||||||
<AudioRecorder
|
<AudioRecorder
|
||||||
|
onRecordingProgress={_ => this.props.onKeyPress(true)}
|
||||||
onDeleted={_ => this.setState({audioRec: false})}
|
onDeleted={_ => this.setState({audioRec: false})}
|
||||||
onFinished={this.handleAttachAudio}/>
|
onFinished={this.handleAttachAudio}/>
|
||||||
</Suspense>) :
|
</Suspense>) :
|
||||||
<textarea id="sendMessage" placeholder={prompt}
|
<textarea id="sendMessage" placeholder={prompt}
|
||||||
value={this.state.message}
|
value={this.state.message} onChange={this.handleMessageTyping}
|
||||||
onChange={this.handleMessageTyping}
|
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
ref={(ref) => {this.messageEditArea = ref;}}
|
ref={(ref) => {this.messageEditArea = ref;}}
|
||||||
autoFocus />)}
|
autoFocus />)}
|
||||||
{this.state.message || !audioEnabled ?
|
{this.state.message || !audioEnabled ?
|
||||||
<a href="#" onClick={this.handleSend} title="Send">
|
<a href="#" onClick={this.handleSend} title={formatMessage(messages.icon_title_send)}>
|
||||||
<i className="material-icons">{sendIcon}</i>
|
<i className="material-icons">{sendIcon}</i>
|
||||||
</a> :
|
</a> :
|
||||||
!this.state.audioRec ?
|
!this.state.audioRec ?
|
||||||
<a href="#" onClick={e => {e.preventDefault(); this.setState({audioRec: true})}} title="Voice">
|
<a href="#" onClick={e => {e.preventDefault(); this.setState({audioRec: true})}} title={formatMessage(messages.icon_title_record_voice)}>
|
||||||
<i className="material-icons">mic</i>
|
<i className="material-icons">mic</i>
|
||||||
</a> :
|
</a> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<input type="file" ref={ref => {this.attachFile = ref}}
|
<input type="file" ref={ref => {this.attachFile = ref}}
|
||||||
onChange={this.handleAttachFile} style={{display: 'none'}} />
|
onChange={this.handleAttachFile} style={{display: 'none'}} />
|
||||||
<input type="file" ref={ref => {this.attachImage = ref}} accept="image/*"
|
<input type="file" ref={ref => {this.attachImage = ref}} accept="image/*, video/*"
|
||||||
onChange={this.handleAttachImage} style={{display: 'none'}} />
|
onChange={this.handleAttachImage} style={{display: 'none'}} />
|
||||||
</>
|
</>
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Image view with progress bar and a cancel button.
|
// Image view with progress bar and a cancel button.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import FileProgress from './file-progress.jsx';
|
import FileProgress from './file-progress.jsx';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import SendMessage from './send-message.jsx';
|
||||||
|
|
||||||
|
export default class VideoPreview extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.videoRef = React.createRef();
|
||||||
|
|
||||||
|
this.handleSendVideo = this.handleSendVideo.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSendVideo(caption) {
|
||||||
|
this.props.onClose();
|
||||||
|
const params = {
|
||||||
|
width: this.videoRef.current.videoWidth,
|
||||||
|
height: this.videoRef.current.videoHeight,
|
||||||
|
duration: (this.videoRef.current.duration * 1000) | 0,
|
||||||
|
mime: this.props.content.mime,
|
||||||
|
name: this.props.content.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture screen from a video.
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = params.width;
|
||||||
|
canvas.height = params.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(this.videoRef.current, 0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.canvas.toBlob(
|
||||||
|
preview => this.props.onSendMessage(caption, this.props.content.blob, preview, params),
|
||||||
|
'image/jpeg', 0.75
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.props.content) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="image-preview">
|
||||||
|
<div id="image-preview-caption-panel">
|
||||||
|
<span>{this.props.content.name}</span>
|
||||||
|
<a href="#" onClick={e => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||||
|
</div>
|
||||||
|
<div id="image-preview-container">
|
||||||
|
<video controls ref={this.videoRef} src={this.props.content.url} className="image-preview" alt={this.props.content.name} />
|
||||||
|
</div>
|
||||||
|
<SendMessage
|
||||||
|
messagePrompt="add_image_caption"
|
||||||
|
acceptBlank={true}
|
||||||
|
tinode={this.props.tinode}
|
||||||
|
reply={this.props.reply}
|
||||||
|
onCancelReply={this.props.onCancelReply}
|
||||||
|
onSendMessage={this.handleSendVideo}
|
||||||
|
onError={this.props.onError} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because one or more lines are too long
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
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
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+702
-268
File diff suppressed because one or more lines are too long
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
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
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,16 +9,19 @@
|
|||||||
|
|
||||||
__webpack_require__.r(__webpack_exports__);
|
__webpack_require__.r(__webpack_exports__);
|
||||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||||
/* harmony export */ "default": () => (/* binding */ AudioRecorder)
|
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
||||||
/* harmony export */ });
|
/* harmony export */ });
|
||||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
|
||||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
||||||
/* harmony import */ var _audio_player_jsx__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./audio-player.jsx */ "./src/widgets/audio-player.jsx");
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-intl */ "react-intl");
|
||||||
/* harmony import */ var webm_duration_fix__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! webm-duration-fix */ "./node_modules/webm-duration-fix/lib/index.js");
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
||||||
/* harmony import */ var webm_duration_fix__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(webm_duration_fix__WEBPACK_IMPORTED_MODULE_2__);
|
/* harmony import */ var _audio_player_jsx__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./audio-player.jsx */ "./src/widgets/audio-player.jsx");
|
||||||
/* harmony import */ var _lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../lib/blob-helpers.js */ "./src/lib/blob-helpers.js");
|
/* harmony import */ var webm_duration_fix__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! webm-duration-fix */ "./node_modules/webm-duration-fix/lib/index.js");
|
||||||
/* harmony import */ var _lib_strformat__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../lib/strformat */ "./src/lib/strformat.js");
|
/* harmony import */ var webm_duration_fix__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(webm_duration_fix__WEBPACK_IMPORTED_MODULE_3__);
|
||||||
/* harmony import */ var _config_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../config.js */ "./src/config.js");
|
/* harmony import */ var _lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../lib/blob-helpers.js */ "./src/lib/blob-helpers.js");
|
||||||
|
/* harmony import */ var _lib_strformat__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../lib/strformat */ "./src/lib/strformat.js");
|
||||||
|
/* harmony import */ var _config_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../config.js */ "./src/config.js");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -35,6 +38,36 @@ const BAR_SCALE = 64.0;
|
|||||||
const VISUALIZATION_BARS = 96;
|
const VISUALIZATION_BARS = 96;
|
||||||
const MAX_SAMPLES_PER_BAR = 10;
|
const MAX_SAMPLES_PER_BAR = 10;
|
||||||
const AUDIO_MIME_TYPE = 'audio/webm';
|
const AUDIO_MIME_TYPE = 'audio/webm';
|
||||||
|
const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
|
||||||
|
icon_title_delete: {
|
||||||
|
id: "icon_title_delete",
|
||||||
|
defaultMessage: [{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Delete recording"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
icon_title_pause: {
|
||||||
|
id: "icon_title_pause",
|
||||||
|
defaultMessage: [{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Pause playback"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
icon_title_resume: {
|
||||||
|
id: "icon_title_resume",
|
||||||
|
defaultMessage: [{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Resume playback"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
icon_title_send: {
|
||||||
|
id: "icon_title_send",
|
||||||
|
defaultMessage: [{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Send message"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) {
|
class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -60,6 +93,7 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
|||||||
this.startedOn = null;
|
this.startedOn = null;
|
||||||
this.viewBuffer = [];
|
this.viewBuffer = [];
|
||||||
this.canvasRef = react__WEBPACK_IMPORTED_MODULE_0___default().createRef();
|
this.canvasRef = react__WEBPACK_IMPORTED_MODULE_0___default().createRef();
|
||||||
|
this.recordingTimestamp = 0;
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.stream = null;
|
this.stream = null;
|
||||||
@@ -102,16 +136,16 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
|||||||
window.requestAnimationFrame(drawFrame);
|
window.requestAnimationFrame(drawFrame);
|
||||||
const duration = this.durationMillis + (Date.now() - this.startedOn);
|
const duration = this.durationMillis + (Date.now() - this.startedOn);
|
||||||
this.setState({
|
this.setState({
|
||||||
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_4__.secondsToTime)(duration / 1000)
|
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_5__.secondsToTime)(duration / 1000)
|
||||||
});
|
});
|
||||||
if (duration > _config_js__WEBPACK_IMPORTED_MODULE_5__.MAX_DURATION) {
|
if (duration > _config_js__WEBPACK_IMPORTED_MODULE_6__.MAX_DURATION) {
|
||||||
this.startedOn = null;
|
this.startedOn = null;
|
||||||
this.mediaRecorder.pause();
|
this.mediaRecorder.pause();
|
||||||
this.durationMillis += Date.now() - this.startedOn;
|
this.durationMillis += Date.now() - this.startedOn;
|
||||||
this.setState({
|
this.setState({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
recording: false,
|
recording: false,
|
||||||
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_4__.secondsToTime)(this.durationMillis / 1000)
|
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_5__.secondsToTime)(this.durationMillis / 1000)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.analyser.getByteTimeDomainData(pcmData);
|
this.analyser.getByteTimeDomainData(pcmData);
|
||||||
@@ -141,6 +175,11 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
|||||||
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
|
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
|
||||||
}
|
}
|
||||||
this.canvasContext.stroke();
|
this.canvasContext.stroke();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (now - this.recordingTimestamp > _config_js__WEBPACK_IMPORTED_MODULE_6__.KEYPRESS_DELAY) {
|
||||||
|
this.props.onRecordingProgress();
|
||||||
|
this.recordingTimestamp = now;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
drawFrame();
|
drawFrame();
|
||||||
}
|
}
|
||||||
@@ -208,8 +247,8 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
|||||||
this.analyser.fftSize = BUFFER_SIZE;
|
this.analyser.fftSize = BUFFER_SIZE;
|
||||||
this.audioInput.connect(this.analyser);
|
this.audioInput.connect(this.analyser);
|
||||||
this.mediaRecorder.onstop = _ => {
|
this.mediaRecorder.onstop = _ => {
|
||||||
if (this.durationMillis > _config_js__WEBPACK_IMPORTED_MODULE_5__.MIN_DURATION) {
|
if (this.durationMillis > _config_js__WEBPACK_IMPORTED_MODULE_6__.MIN_DURATION) {
|
||||||
this.getRecording(this.mediaRecorder.mimeType, this.durationMillis).then(result => this.props.onFinished(result.url, result.preview, this.durationMillis));
|
this.getRecording(this.mediaRecorder.mimeType).then(result => this.props.onFinished(result.url, result.preview, this.durationMillis));
|
||||||
} else {
|
} else {
|
||||||
this.props.onDeleted();
|
this.props.onDeleted();
|
||||||
}
|
}
|
||||||
@@ -232,18 +271,20 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
|||||||
this.startedOn = Date.now();
|
this.startedOn = Date.now();
|
||||||
this.mediaRecorder.start();
|
this.mediaRecorder.start();
|
||||||
this.visualize();
|
this.visualize();
|
||||||
|
this.props.onRecordingProgress();
|
||||||
|
this.recordingTimestamp = this.startedOn;
|
||||||
}
|
}
|
||||||
getRecording(mimeType, duration) {
|
getRecording(mimeType) {
|
||||||
mimeType = mimeType || AUDIO_MIME_TYPE;
|
mimeType = mimeType || AUDIO_MIME_TYPE;
|
||||||
let blob = new Blob(this.audioChunks, {
|
let blob = new Blob(this.audioChunks, {
|
||||||
type: mimeType
|
type: mimeType
|
||||||
});
|
});
|
||||||
return webm_duration_fix__WEBPACK_IMPORTED_MODULE_2___default()(blob, mimeType).then(fixedBlob => {
|
return webm_duration_fix__WEBPACK_IMPORTED_MODULE_3___default()(blob, mimeType).then(fixedBlob => {
|
||||||
blob = fixedBlob;
|
blob = fixedBlob;
|
||||||
return fixedBlob.arrayBuffer();
|
return fixedBlob.arrayBuffer();
|
||||||
}).then(arrayBuff => this.audioContext.decodeAudioData(arrayBuff)).then(decoded => this.createPreview(decoded)).then(preview => ({
|
}).then(arrayBuff => this.audioContext.decodeAudioData(arrayBuff)).then(decoded => this.createPreview(decoded)).then(preview => ({
|
||||||
url: window.URL.createObjectURL(blob),
|
url: window.URL.createObjectURL(blob),
|
||||||
preview: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_3__.intArrayToBase64)(preview)
|
preview: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.intArrayToBase64)(preview)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
createPreview(audio) {
|
createPreview(audio) {
|
||||||
@@ -274,18 +315,21 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
|||||||
this.stream.getTracks().forEach(track => track.stop());
|
this.stream.getTracks().forEach(track => track.stop());
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
formatMessage
|
||||||
|
} = this.props.intl;
|
||||||
const resumeClass = 'material-icons ' + (this.state.enabled ? 'red' : 'gray');
|
const resumeClass = 'material-icons ' + (this.state.enabled ? 'red' : 'gray');
|
||||||
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||||
className: "audio"
|
className: "audio"
|
||||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||||
href: "#",
|
href: "#",
|
||||||
onClick: this.handleDelete,
|
onClick: this.handleDelete,
|
||||||
title: "Delete"
|
title: formatMessage(messages.icon_title_delete)
|
||||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||||
className: "material-icons gray"
|
className: "material-icons gray"
|
||||||
}, "delete_outline")), this.state.recording ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("canvas", {
|
}, "delete_outline")), this.state.recording ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("canvas", {
|
||||||
ref: this.canvasRef
|
ref: this.canvasRef
|
||||||
}) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_audio_player_jsx__WEBPACK_IMPORTED_MODULE_1__["default"], {
|
}) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_audio_player_jsx__WEBPACK_IMPORTED_MODULE_2__["default"], {
|
||||||
src: this.state.blobUrl,
|
src: this.state.blobUrl,
|
||||||
preview: this.state.preview,
|
preview: this.state.preview,
|
||||||
duration: this.durationMillis,
|
duration: this.durationMillis,
|
||||||
@@ -295,24 +339,25 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
|||||||
}, this.state.duration), this.state.recording ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
}, this.state.duration), this.state.recording ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||||
href: "#",
|
href: "#",
|
||||||
onClick: this.handlePause,
|
onClick: this.handlePause,
|
||||||
title: "Pause"
|
title: formatMessage(messages.icon_title_pause)
|
||||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||||
className: "material-icons"
|
className: "material-icons"
|
||||||
}, "pause_circle_outline")) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
}, "pause_circle_outline")) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||||
href: "#",
|
href: "#",
|
||||||
onClick: this.handleResume,
|
onClick: this.handleResume,
|
||||||
title: "Resume"
|
title: formatMessage(messages.icon_title_resume)
|
||||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||||
className: resumeClass
|
className: resumeClass
|
||||||
}, "radio_button_checked")), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
}, "radio_button_checked")), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||||
href: "#",
|
href: "#",
|
||||||
onClick: this.handleDone,
|
onClick: this.handleDone,
|
||||||
title: "Send"
|
title: formatMessage(messages.icon_title_send)
|
||||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||||
className: "material-icons"
|
className: "material-icons"
|
||||||
}, "send")));
|
}, "send")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ((0,react_intl__WEBPACK_IMPORTED_MODULE_1__.injectIntl)(AudioRecorder));
|
||||||
|
|
||||||
/***/ })
|
/***/ })
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+136
-513
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user