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
|
||||
package-lock.json binary
|
||||
*.min.css binary
|
||||
|
||||
+80
-1
@@ -400,6 +400,10 @@ div.trusted-badge {
|
||||
color: #388e3c; /* green 700 */
|
||||
}
|
||||
|
||||
.white {
|
||||
color: #FFF!important; /* 100% white */
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #D32F2F!important; /* red 700 */
|
||||
}
|
||||
@@ -1878,6 +1882,7 @@ video {
|
||||
font-size: smaller;
|
||||
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
|
||||
background-color: rgb(244 244 244 / 66%);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Left triangle placed bottom left flush. */
|
||||
@@ -1985,6 +1990,7 @@ video {
|
||||
|
||||
/* Inline and preview image styles */
|
||||
.inline-image, .image-preview {
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 0.1rem;
|
||||
@@ -2022,7 +2028,7 @@ img.inline-image::before, img.image-preview::before {
|
||||
|
||||
img.inline-image::before {
|
||||
content: attr(alt);
|
||||
background-size: 2rem 2rem;
|
||||
background-size: 3rem 3rem;
|
||||
}
|
||||
|
||||
img.image-preview::before {
|
||||
@@ -2030,6 +2036,48 @@ img.image-preview::before {
|
||||
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 {
|
||||
display: flex;
|
||||
@@ -2470,6 +2518,9 @@ ul.tabbar li.active > a {
|
||||
.material-icons.big {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
.material-icons.bigger {
|
||||
font-size: 3rem;
|
||||
}
|
||||
.material-icons.huge {
|
||||
font-size: 4rem;
|
||||
}
|
||||
@@ -2924,6 +2975,34 @@ ul.tabbar li.active > a {
|
||||
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.
|
||||
*/
|
||||
|
||||
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-dom": "^18.2.0",
|
||||
"react-intl": "^6.2.1",
|
||||
"tinode-sdk": "^0.20.2",
|
||||
"tinode-sdk": "^0.20.3",
|
||||
"webm-duration-fix": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -2423,9 +2423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz",
|
||||
"integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.0.tgz",
|
||||
"integrity": "sha512-ySMTXQuMvvswoobvN+0LsaPf7ITO2JVfJmHxQKI4cGehNrrUms+n81BlHEX7Hl/LExji6XE3fnI9U04GSkRruA==",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.0",
|
||||
"@types/node": ">=12.12.47"
|
||||
@@ -2435,9 +2435,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz",
|
||||
"integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==",
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz",
|
||||
"integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==",
|
||||
"dependencies": {
|
||||
"@types/long": "^4.0.1",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
@@ -6266,9 +6266,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tinode-sdk": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/tinode-sdk/-/tinode-sdk-0.20.2.tgz",
|
||||
"integrity": "sha512-aazufXwxwFx6VMByRMe7LeUgqDKiCEqRuf8DJO+mD0CdL1FVQP2mcC91ZQmORSUXtBX5lWYtl6TDPDZOVCS/lg=="
|
||||
"version": "0.20.3",
|
||||
"resolved": "https://registry.npmjs.org/tinode-sdk/-/tinode-sdk-0.20.3.tgz",
|
||||
"integrity": "sha512-xJORATgCwCsILp2JmCvVRZxlxdN9bXjtRETHfFrBU40HxoP9z6Nk48KnDdTVvzSxwHnfwtxzj9B1KUl6Q1tRaQ=="
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.1",
|
||||
@@ -6313,9 +6313,9 @@
|
||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
||||
"version": "4.9.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
|
||||
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -8518,18 +8518,18 @@
|
||||
}
|
||||
},
|
||||
"@grpc/grpc-js": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz",
|
||||
"integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.0.tgz",
|
||||
"integrity": "sha512-ySMTXQuMvvswoobvN+0LsaPf7ITO2JVfJmHxQKI4cGehNrrUms+n81BlHEX7Hl/LExji6XE3fnI9U04GSkRruA==",
|
||||
"requires": {
|
||||
"@grpc/proto-loader": "^0.7.0",
|
||||
"@types/node": ">=12.12.47"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz",
|
||||
"integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==",
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz",
|
||||
"integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==",
|
||||
"requires": {
|
||||
"@types/long": "^4.0.1",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
@@ -11350,9 +11350,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"tinode-sdk": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/tinode-sdk/-/tinode-sdk-0.20.2.tgz",
|
||||
"integrity": "sha512-aazufXwxwFx6VMByRMe7LeUgqDKiCEqRuf8DJO+mD0CdL1FVQP2mcC91ZQmORSUXtBX5lWYtl6TDPDZOVCS/lg=="
|
||||
"version": "0.20.3",
|
||||
"resolved": "https://registry.npmjs.org/tinode-sdk/-/tinode-sdk-0.20.3.tgz",
|
||||
"integrity": "sha512-xJORATgCwCsILp2JmCvVRZxlxdN9bXjtRETHfFrBU40HxoP9z6Nk48KnDdTVvzSxwHnfwtxzj9B1KUl6Q1tRaQ=="
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.2.1",
|
||||
@@ -11388,9 +11388,9 @@
|
||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
||||
"version": "4.9.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
|
||||
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
|
||||
"devOptional": true
|
||||
},
|
||||
"unicode-canonical-property-names-ecmascript": {
|
||||
|
||||
+1
-3
@@ -2,7 +2,6 @@
|
||||
"name": "tinode-webapp",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.21.0-beta1",
|
||||
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinode/example-react-js.git"
|
||||
@@ -68,7 +67,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-intl": "^6.2.1",
|
||||
"tinode-sdk": "^0.20.2",
|
||||
"tinode-sdk": "^0.21.0-beta1",
|
||||
"webm-duration-fix": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -82,7 +81,6 @@
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"cssnano": "^5.1.14",
|
||||
"glob": "^8.0.3",
|
||||
"mkdirp": "^1.0.4",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-cli": "^10.0.0",
|
||||
"webpack": "^5.75.0",
|
||||
|
||||
+1
-7
@@ -5,13 +5,11 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const globSync = require('glob').sync;
|
||||
const mkdirpSync = require('mkdirp').sync;
|
||||
|
||||
const EXTRACTED_STRINGS = './src/i18n/ex/base-en.json';
|
||||
const BASE_LANG = 'en';
|
||||
const LANG_PATTERN = './src/i18n/*.json';
|
||||
const LANG_PATTERN_ONE = './src/i18n/%s.json';
|
||||
const FINAL_FILE = './src/messages.json';
|
||||
const OUTPUT_PATTERN = './src/i18n.min/%s.json';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -123,11 +121,7 @@ const messages = globSync(LANG_PATTERN)
|
||||
return { ...collection, ...descriptors };
|
||||
}, {});
|
||||
|
||||
// Legacy: Write a single file with all translations combined.
|
||||
mkdirpSync(path.dirname(FINAL_FILE));
|
||||
fs.writeFileSync(FINAL_FILE, JSON.stringify(messages));
|
||||
|
||||
// New way: write minified ptraslation files.
|
||||
// Write minified translation files, one per language.
|
||||
for (let lang in messages) {
|
||||
fs.writeFileSync(printf(OUTPUT_PATTERN, lang), JSON.stringify(messages[lang]));
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
import { PACKAGE_VERSION } from './version.js';
|
||||
|
||||
// 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
|
||||
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.
|
||||
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.
|
||||
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,
|
||||
"obsolete": false
|
||||
},
|
||||
"deleted_content": {
|
||||
"translation": "Inhalt gelöscht",
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"download_action": {
|
||||
"translation": "herunterladen",
|
||||
"defaultMessage": "download",
|
||||
@@ -1472,5 +1465,68 @@
|
||||
"description": "Marker indicating that the message was edited",
|
||||
"missing": 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,
|
||||
"obsolete": false
|
||||
},
|
||||
"deleted_content": {
|
||||
"translation": "content deleted",
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"download_action": {
|
||||
"translation": "download",
|
||||
"defaultMessage": "download",
|
||||
@@ -1472,5 +1465,68 @@
|
||||
"description": "Marker indicating that the message was edited",
|
||||
"missing": 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,
|
||||
"obsolete": false
|
||||
},
|
||||
"deleted_content": {
|
||||
"translation": "este mensaje fue eliminado",
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"download_action": {
|
||||
"translation": "descargar",
|
||||
"defaultMessage": "download",
|
||||
@@ -1472,5 +1465,68 @@
|
||||
"description": "Marker indicating that the message was edited",
|
||||
"missing": 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.",
|
||||
"description": "Alert dialog warning when hard-deleting all messages."
|
||||
},
|
||||
"deleted_content": {
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted"
|
||||
},
|
||||
"description_editing_placeholder": {
|
||||
"defaultMessage": "Description (optional)",
|
||||
"description": "Placeholder for editing topic description"
|
||||
@@ -243,6 +239,10 @@
|
||||
"defaultMessage": "Unsupported",
|
||||
"description": "Unsupported entity in drafty"
|
||||
},
|
||||
"drag_file": {
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner"
|
||||
},
|
||||
"editing_message": {
|
||||
"defaultMessage": "Editing",
|
||||
"description": "Title over message editing preview"
|
||||
@@ -298,6 +298,38 @@
|
||||
"defaultMessage": "Group or User 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": {
|
||||
"defaultMessage": "Image caption",
|
||||
"description": "Prompt in SendMessage for attached image"
|
||||
|
||||
+63
-7
@@ -307,13 +307,6 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"deleted_content": {
|
||||
"translation": "contenu supprimé",
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"download_action": {
|
||||
"translation": "télécharger",
|
||||
"defaultMessage": "download",
|
||||
@@ -1472,5 +1465,68 @@
|
||||
"description": "Marker indicating that the message was edited",
|
||||
"missing": 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,
|
||||
"obsolete": false
|
||||
},
|
||||
"deleted_content": {
|
||||
"translation": "내용이 삭제되었습니다",
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"download_action": {
|
||||
"translation": "다운로드",
|
||||
"defaultMessage": "download",
|
||||
@@ -1472,5 +1465,68 @@
|
||||
"description": "Marker indicating that the message was edited",
|
||||
"missing": 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,
|
||||
"obsolete": false
|
||||
},
|
||||
"deleted_content": {
|
||||
"translation": "conținut șters",
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"download_action": {
|
||||
"translation": "download",
|
||||
"defaultMessage": "download",
|
||||
@@ -1472,5 +1465,68 @@
|
||||
"description": "Marker indicating that the message was edited",
|
||||
"missing": 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,
|
||||
"obsolete": false
|
||||
},
|
||||
"deleted_content": {
|
||||
"translation": "удалено",
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"download_action": {
|
||||
"translation": "скачать",
|
||||
"defaultMessage": "download",
|
||||
@@ -1472,5 +1465,68 @@
|
||||
"description": "Marker indicating that the message was edited",
|
||||
"missing": 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,
|
||||
"obsolete": false
|
||||
},
|
||||
"deleted_content": {
|
||||
"translation": "內容已被刪除",
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"download_action": {
|
||||
"translation": "下載",
|
||||
"defaultMessage": "download",
|
||||
@@ -1472,5 +1465,68 @@
|
||||
"description": "Marker indicating that the message was edited",
|
||||
"missing": 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,
|
||||
"obsolete": false
|
||||
},
|
||||
"deleted_content": {
|
||||
"translation": "内容已删除",
|
||||
"defaultMessage": "content deleted",
|
||||
"description": "Shown when messages are deleted",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"download_action": {
|
||||
"translation": "下载",
|
||||
"defaultMessage": "download",
|
||||
@@ -1472,5 +1465,68 @@
|
||||
"description": "Marker indicating that the message was edited",
|
||||
"missing": 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
|
||||
}
|
||||
}
|
||||
|
||||
+4
-9
@@ -6,12 +6,10 @@ import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import allMessages from './messages.json';
|
||||
import TinodeWeb from './views/tinode-web.jsx';
|
||||
import HashNavigation from './lib/navigation.js';
|
||||
|
||||
/*
|
||||
// Load just one language.
|
||||
// Allow loading translation strings for just one language.
|
||||
const messageLoader = {
|
||||
'de': _ => import('./i18n.min/de.json'),
|
||||
'en': _ => import('./i18n.min/en.json'),
|
||||
@@ -23,7 +21,6 @@ const messageLoader = {
|
||||
'zh': _ => import('./i18n.min/zh.json'),
|
||||
'zh-TW': _ => import('./i18n.min/zh-TW.json')
|
||||
};
|
||||
*/
|
||||
|
||||
// Detect human language to use in the UI:
|
||||
// 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();
|
||||
|
||||
// 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 messages = await messageLoader[htmlLang]();
|
||||
const htmlLang = allMessages[normalized] ? language : allMessages[baseLanguage] ? baseLanguage : 'en';
|
||||
const messages = allMessages[htmlLang];
|
||||
const htmlLang = messageLoader[normalized] ? language : messageLoader[baseLanguage] ? baseLanguage : 'en';
|
||||
|
||||
// Set lang attribute of the HTML element: <html lang="XX">
|
||||
document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
|
||||
|
||||
// Render the app.
|
||||
const root = createRoot(document.getElementById('mountPoint'));
|
||||
messageLoader[htmlLang]().then(messages =>
|
||||
root.render(
|
||||
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
|
||||
<TinodeWeb />
|
||||
</IntlProvider>
|
||||
);
|
||||
));
|
||||
|
||||
@@ -172,7 +172,7 @@ export function imageCrop(mime, objURL, left, top, width, height, scale) {
|
||||
export function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = (evt) => {
|
||||
reader.onerror = _ => {
|
||||
reject(reader.error);
|
||||
};
|
||||
reader.onload = _ => {
|
||||
|
||||
+97
-49
@@ -6,6 +6,7 @@ import { Drafty } from 'tinode-sdk';
|
||||
import AudioPlayer from '../widgets/audio-player.jsx'
|
||||
import CallMessage from '../widgets/call-message.jsx'
|
||||
import CallStatus from '../widgets/call-status.jsx';
|
||||
import InlineVideo from '../widgets/inline-video.jsx';
|
||||
import LazyImage from '../widgets/lazy-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.
|
||||
// 'this' must contain:
|
||||
// viewportWidth: this.props.viewportWidth;
|
||||
// authorizeURL: this.props.tinode.authorizeURL
|
||||
// onImagePreview: this.handleImagePreview
|
||||
// onFormButtonClick: this.handleFormButtonClick
|
||||
// onQuoteClick: this.handleQuoteClick (optional)
|
||||
// viewportWidth:
|
||||
// authorizeURL:
|
||||
// onImagePreview:
|
||||
// onVideoPreview:
|
||||
// onFormButtonClick:
|
||||
// onQuoteClick:
|
||||
export function fullFormatter(style, data, values, key, stack) {
|
||||
if (stack.includes('QQ')) {
|
||||
return quoteFormatter.call(this, style, data, values, key);
|
||||
@@ -175,6 +133,12 @@ export function fullFormatter(style, data, values, key, stack) {
|
||||
attr.duration = data.duration;
|
||||
}
|
||||
break;
|
||||
case 'VD':
|
||||
// Additional processing for videos.
|
||||
el = handleVideoData.call(this, el, data, attr);
|
||||
// Video element cannot have content.
|
||||
values = null;
|
||||
break;
|
||||
default:
|
||||
if (!el) {
|
||||
// Unknown element.
|
||||
@@ -195,6 +159,90 @@ export function fullFormatter(style, data, values, key, stack) {
|
||||
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.
|
||||
// 'this' must contain:
|
||||
// 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'});
|
||||
}
|
||||
|
||||
// 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.
|
||||
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
|
||||
export function secondsToTime(seconds, fixedMin) {
|
||||
let min = Math.floor(seconds / 60) | 0;
|
||||
let hours = Math.floor(min / 60) | 0;
|
||||
if (typeof seconds != 'number') {
|
||||
return '';
|
||||
}
|
||||
|
||||
let min = (Math.floor(seconds / 60) | 0) % 60;
|
||||
let hours = Math.floor(seconds / 36000) | 0;
|
||||
|
||||
if (fixedMin || hours > 0) {
|
||||
min = min < 10 ? `0${min}` : min;
|
||||
}
|
||||
let sec = (seconds % 60) | 0;
|
||||
let sec = (seconds | 0) % 60;
|
||||
sec = sec < 10 ? `0${sec}` : sec;
|
||||
if (hours == 0) {
|
||||
return `${min}:${sec}`;
|
||||
|
||||
File diff suppressed because one or more lines are too long
+210
-63
@@ -9,6 +9,7 @@ import CallPanel from '../widgets/call-panel.jsx';
|
||||
import ChatMessage from '../widgets/chat-message.jsx';
|
||||
import ContactBadges from '../widgets/contact-badges.jsx';
|
||||
import DocPreview from '../widgets/doc-preview.jsx';
|
||||
import DragAndDrop from '../widgets/drag-and-drop.jsx'
|
||||
import ErrorPanel from '../widgets/error-panel.jsx';
|
||||
import GroupSubs from '../widgets/group-subs.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 MetaMessage from '../widgets/meta-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,
|
||||
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 { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.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.
|
||||
const NOTIFICATION_EXEC_INTERVAL = 300;
|
||||
@@ -67,6 +69,11 @@ const messages = defineMessages({
|
||||
id: 'editing_message',
|
||||
defaultMessage: 'Editing',
|
||||
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.retrySend = this.retrySend.bind(this);
|
||||
this.sendImageAttachment = this.sendImageAttachment.bind(this);
|
||||
this.sendVideoAttachment = this.sendVideoAttachment.bind(this);
|
||||
this.sendFileAttachment = this.sendFileAttachment.bind(this);
|
||||
this.sendAudioAttachment = this.sendAudioAttachment.bind(this);
|
||||
this.sendKeyPress = this.sendKeyPress.bind(this);
|
||||
@@ -123,11 +131,12 @@ class MessagesView extends React.Component {
|
||||
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
||||
this.handleEnablePeer = this.handleEnablePeer.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.postReadNotification = this.postReadNotification.bind(this);
|
||||
this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
|
||||
this.goToLatestMessage = this.goToLatestMessage.bind(this);
|
||||
this.handleFileDrop = this.handleFileDrop.bind(this);
|
||||
|
||||
this.handlePickReply = this.handlePickReply.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) {
|
||||
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) {
|
||||
@@ -743,12 +755,16 @@ class MessagesView extends React.Component {
|
||||
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);
|
||||
if (topic.isSubscribed()) {
|
||||
if (audio) {
|
||||
topic.noteRecording(true);
|
||||
} else {
|
||||
topic.noteKeyPress();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendMessage sends the message with an optional subscription to topic first.
|
||||
sendMessage(msg, uploadCompletionPromise, uploader) {
|
||||
@@ -819,7 +835,7 @@ class MessagesView extends React.Component {
|
||||
// Small enough to send inband.
|
||||
fileToBase64(file)
|
||||
.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);
|
||||
}
|
||||
|
||||
// sendImageAttachment sends the image bits inband as Drafty message.
|
||||
// sendImageAttachment sends the image bits as Drafty message.
|
||||
sendImageAttachment(caption, blob) {
|
||||
const mime = this.state.imagePreview.mime;
|
||||
const width = this.state.imagePreview.width;
|
||||
@@ -879,6 +895,7 @@ class MessagesView extends React.Component {
|
||||
let msg = Drafty.insertImage(null, 0, {
|
||||
mime: mime,
|
||||
_tempPreview: b64.bits, // This preview will not be serialized.
|
||||
bits: b64.bits, // Image thumbnail.
|
||||
width: width,
|
||||
height: height,
|
||||
filename: fname,
|
||||
@@ -896,12 +913,13 @@ class MessagesView extends React.Component {
|
||||
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)
|
||||
.then(b64 => {
|
||||
let msg = Drafty.insertImage(null, 0, {
|
||||
mime: b64.mime,
|
||||
preview: b64.bits, // Serializable preview
|
||||
bits: b64.bits,
|
||||
width: width,
|
||||
height: height,
|
||||
filename: fname,
|
||||
@@ -915,10 +933,107 @@ class MessagesView extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
// handleAttachImage method is called when [Attach image] button is clicked: launch image preview.
|
||||
handleAttachImage(file) {
|
||||
// sendVideoAttachment sends the video bits as Drafty message.
|
||||
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);
|
||||
|
||||
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.
|
||||
imageScaled(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, maxExternAttachmentSize, false)
|
||||
.then(scaled => {
|
||||
@@ -932,10 +1047,23 @@ class MessagesView extends React.Component {
|
||||
mime: scaled.mime
|
||||
}});
|
||||
}).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(url, preview, duration) {
|
||||
fetch(url)
|
||||
@@ -966,15 +1094,15 @@ class MessagesView extends React.Component {
|
||||
.then(b64 => {
|
||||
this.sendMessage(Drafty.appendAudio(null, {
|
||||
mime: b64.mime,
|
||||
bits: b64.bits,
|
||||
size: blob.size,
|
||||
data: b64.bits,
|
||||
duration: duration,
|
||||
preview: preview,
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(err => {this.props.onError(err)});;
|
||||
.catch(err => {this.props.onError(err.message, 'err')});;
|
||||
}
|
||||
|
||||
handleCancelUpload(seq, uploader) {
|
||||
@@ -1085,6 +1213,17 @@ class MessagesView extends React.Component {
|
||||
onClose={this.handleClosePreview}
|
||||
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) {
|
||||
// Expand received image.
|
||||
component2 = (
|
||||
@@ -1141,7 +1280,6 @@ class MessagesView extends React.Component {
|
||||
let previousFrom = null;
|
||||
let prevDate = null;
|
||||
let chatBoxClass = null;
|
||||
const dateFmt = new Intl.DateTimeFormat(this.props.intl.locale);
|
||||
topic.messages((msg, prev, next, i) => {
|
||||
let nextFrom = next ? (next.from || 'chan') : null;
|
||||
|
||||
@@ -1189,7 +1327,7 @@ class MessagesView extends React.Component {
|
||||
if (!prevDate || prevDate.toDateString() != thisDate.toDateString()) {
|
||||
messageNodes.push(
|
||||
<MetaMessage
|
||||
date={dateFmt.format(msg.ts)}
|
||||
date={relativeDateFormat(msg.ts)}
|
||||
locale={this.props.intl.locale}
|
||||
key={'date-' + msg.seq} />
|
||||
);
|
||||
@@ -1250,6 +1388,59 @@ class MessagesView extends React.Component {
|
||||
|
||||
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 = (
|
||||
<>
|
||||
<div id="topic-caption-panel" className="caption-panel">
|
||||
@@ -1295,53 +1486,9 @@ class MessagesView extends React.Component {
|
||||
onClearError={this.props.onError} />
|
||||
: null}
|
||||
<LoadSpinner show={this.state.fetchingMessages} />
|
||||
<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}
|
||||
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} />}
|
||||
{!this.state.unconfirmed && !this.props.forwardMessage ?
|
||||
<DragAndDrop actionPrompt={this.props.intl.formatMessage(messages.drag_file)} onDrop={this.handleFileDrop} >{messagesComponent}</DragAndDrop>
|
||||
: messagesComponent}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ class TinodeWeb extends React.Component {
|
||||
this.handleCallIceCandidate = this.handleCallIceCandidate.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);
|
||||
|
||||
@@ -1791,7 +1791,7 @@ class TinodeWeb extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleCallAcceptCall(topicName) {
|
||||
handleCallAccept(topicName) {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
if (!topic) {
|
||||
return;
|
||||
@@ -1914,7 +1914,7 @@ class TinodeWeb extends React.Component {
|
||||
seq={this.state.callSeq}
|
||||
callState={this.state.callState}
|
||||
onRinging={this.handleCallRinging}
|
||||
onAcceptCall={this.handleCallAcceptCall}
|
||||
onAcceptCall={this.handleCallAccept}
|
||||
onReject={this.handleCallHangup}
|
||||
/>
|
||||
:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Audio recorder widget.
|
||||
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { secondsToTime } from '../lib/strformat';
|
||||
import { base64ToIntArray } from '../lib/blob-helpers';
|
||||
@@ -18,7 +19,15 @@ const THUMB_COLOR = '#444E';
|
||||
// Minimum number of amplitude bars to draw.
|
||||
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) {
|
||||
super(props);
|
||||
|
||||
@@ -237,7 +246,7 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
const playClass = 'material-icons' +
|
||||
(this.props.short ? '' : ' large') +
|
||||
(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' :
|
||||
(this.state.canPlay ? 'play_circle' : 'not_interested')}</i>
|
||||
</a>);
|
||||
@@ -258,3 +267,5 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(AudioPlayer);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Audio recorder widget.
|
||||
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import AudioPlayer from './audio-player.jsx';
|
||||
// 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 { secondsToTime } from '../lib/strformat';
|
||||
import { MAX_DURATION, MIN_DURATION } from '../config.js';
|
||||
import { KEYPRESS_DELAY, MAX_DURATION, MIN_DURATION } from '../config.js';
|
||||
|
||||
// FFT resolution.
|
||||
const BUFFER_SIZE = 256;
|
||||
@@ -33,7 +34,30 @@ const MAX_SAMPLES_PER_BAR = 10;
|
||||
// Recording format.
|
||||
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) {
|
||||
super(props);
|
||||
|
||||
@@ -62,6 +86,9 @@ export default class AudioRecorder extends React.PureComponent {
|
||||
this.startedOn = null;
|
||||
this.viewBuffer = [];
|
||||
this.canvasRef = React.createRef();
|
||||
|
||||
// Timestamp for sending "recording" notifications.
|
||||
this.recordingTimestamp = 0;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -168,6 +195,13 @@ export default class AudioRecorder extends React.PureComponent {
|
||||
}
|
||||
// Actually draw the bars on canvas.
|
||||
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();
|
||||
@@ -238,7 +272,7 @@ export default class AudioRecorder extends React.PureComponent {
|
||||
|
||||
this.mediaRecorder.onstop = _ => {
|
||||
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));
|
||||
} else {
|
||||
this.props.onDeleted();
|
||||
@@ -265,11 +299,14 @@ export default class AudioRecorder extends React.PureComponent {
|
||||
this.startedOn = Date.now();
|
||||
this.mediaRecorder.start();
|
||||
this.visualize();
|
||||
|
||||
this.props.onRecordingProgress();
|
||||
this.recordingTimestamp = this.startedOn;
|
||||
}
|
||||
|
||||
// Obtain data in a form sutable for sending or playing back.
|
||||
// If duration is valid, apply fix for Chrome's WebM duration bug.
|
||||
getRecording(mimeType, duration) {
|
||||
getRecording(mimeType) {
|
||||
mimeType = mimeType || AUDIO_MIME_TYPE;
|
||||
let blob = new Blob(this.audioChunks, {type: mimeType});
|
||||
return fixWebmDuration(blob, mimeType)
|
||||
@@ -315,10 +352,11 @@ export default class AudioRecorder extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const resumeClass = 'material-icons ' + (this.state.enabled ? 'red' : 'gray');
|
||||
return (
|
||||
<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>
|
||||
</a>
|
||||
{this.state.recording ?
|
||||
@@ -332,17 +370,19 @@ export default class AudioRecorder extends React.PureComponent {
|
||||
}
|
||||
<div className="duration">{this.state.duration}</div>
|
||||
{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>
|
||||
</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>
|
||||
</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>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(AudioRecorder);
|
||||
|
||||
@@ -37,7 +37,7 @@ export default class CallIncoming extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetDesc(topic, this.props);
|
||||
this.resetDesc(topic);
|
||||
if (this.props.callState == CALL_STATE_INCOMING_RECEIVED) {
|
||||
// play() throws if the user did not click the app first: https://goo.gl/xX8pDD.
|
||||
RING_SOUND.play().catch(_ => {});
|
||||
@@ -77,9 +77,7 @@ export default class CallIncoming extends React.Component {
|
||||
topic.onMetaDesc = this.previousMetaDesc;
|
||||
}
|
||||
|
||||
resetDesc(topic, props) {
|
||||
const defacs = topic.getDefaultAccess() || {};
|
||||
const acs = topic.getAccessMode();
|
||||
resetDesc(topic) {
|
||||
const badges = [];
|
||||
if (topic.trusted) {
|
||||
for (const [key, val] of Object.entries(topic.trusted)) {
|
||||
@@ -101,7 +99,7 @@ export default class CallIncoming extends React.Component {
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
this.resetDesc(topic, this.props);
|
||||
this.resetDesc(topic);
|
||||
|
||||
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
|
||||
this.previousMetaDesc(desc);
|
||||
|
||||
@@ -421,14 +421,14 @@ class CallPanel extends React.PureComponent {
|
||||
<div id="video-container">
|
||||
<div id="video-container-panel">
|
||||
<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">
|
||||
<FormattedMessage id="calls_you_label"
|
||||
defaultMessage="You" description="Shown over the local video screen" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="call-party peer">
|
||||
<video ref={this.remoteRef} autoPlay playsInline></video>
|
||||
<video ref={this.remoteRef} autoPlay playsInline />
|
||||
{remoteActive ?
|
||||
<div className="caller-name inactive">{peerTitle}</div> :
|
||||
<div className={`caller-card${pulseAnimation}`}>
|
||||
|
||||
@@ -35,6 +35,7 @@ class BaseChatMessage extends React.PureComponent {
|
||||
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
|
||||
|
||||
onImagePreview: this.handleImagePreview,
|
||||
onVideoPreview: this.handleImagePreview,
|
||||
onFormButtonClick: this.handleFormButtonClick,
|
||||
onQuoteClick: this.handleQuoteClick
|
||||
};
|
||||
@@ -76,14 +77,19 @@ class BaseChatMessage extends React.PureComponent {
|
||||
if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
|
||||
menuItems.push('menu_item_send_retry');
|
||||
}
|
||||
if (this.props.userIsWriter &&
|
||||
this.props.received > Tinode.MESSAGE_STATUS_FAILED &&
|
||||
this.props.received < Tinode.MESSAGE_STATUS_DEL_RANGE) {
|
||||
if (this.props.userIsWriter && this.props.received > Tinode.MESSAGE_STATUS_FAILED) {
|
||||
menuItems.push('menu_item_reply');
|
||||
if (!this.props.response) {
|
||||
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');
|
||||
|
||||
this.props.showContextMenu({
|
||||
@@ -146,12 +152,6 @@ class BaseChatMessage extends React.PureComponent {
|
||||
}, this);
|
||||
const tree = Drafty.format(content, fullFormatter, this.formatterContext);
|
||||
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') {
|
||||
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
|
||||
<FormattedMessage id="invalid_content"
|
||||
|
||||
@@ -80,7 +80,7 @@ class ContactList extends React.Component {
|
||||
let previewIsResponse;
|
||||
let deliveryStatus;
|
||||
if (!this.props.showMode && c.latestMessage) {
|
||||
const msg = c.latestMessage(true);
|
||||
const msg = c.latestMessage();
|
||||
if (msg) {
|
||||
forwarded = msg.head ? msg.head.forwarded : null;
|
||||
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 { FormattedMessage } from 'react-intl';
|
||||
import SendMessage from '../widgets/send-message.jsx';
|
||||
import SendMessage from './send-message.jsx';
|
||||
|
||||
import { REM_SIZE } from '../config.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.
|
||||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class MetaMessage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -11,14 +10,7 @@ export default class MetaMessage extends React.PureComponent {
|
||||
render() {
|
||||
let content = null;
|
||||
let bubbleClass = 'bubble';
|
||||
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></>
|
||||
bubbleClass += ' deleted';
|
||||
} else if (this.props.date) {
|
||||
if (this.props.date) {
|
||||
// A date badge.
|
||||
content = <>{this.props.date}</>
|
||||
bubbleClass += ' date';
|
||||
|
||||
@@ -37,6 +37,26 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Cannot initiate file upload.',
|
||||
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 {
|
||||
@@ -48,10 +68,11 @@ class SendMessage extends React.PureComponent {
|
||||
message: '',
|
||||
audioRec: false,
|
||||
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.handleAttachImage = this.handleAttachImage.bind(this);
|
||||
this.handleAttachFile = this.handleAttachFile.bind(this);
|
||||
@@ -59,6 +80,7 @@ class SendMessage extends React.PureComponent {
|
||||
this.handleSend = this.handleSend.bind(this);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
this.handleMessageTyping = this.handleMessageTyping.bind(this);
|
||||
this.handleDropAttach = this.handleDropAttach.bind(this);
|
||||
|
||||
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
||||
|
||||
@@ -138,6 +160,13 @@ class SendMessage extends React.PureComponent {
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
handleDropAttach(files) {
|
||||
if (files && files.length > 0) {
|
||||
console.log('Dropping ', files);
|
||||
this.props.onAttachFile(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
handleAttachAudio(url, preview, duration) {
|
||||
this.setState({audioRec: false});
|
||||
this.props.onAttachAudio(url, preview, duration);
|
||||
@@ -174,15 +203,14 @@ class SendMessage extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleMessageTyping(e) {
|
||||
const newState = {message: e.target.value};
|
||||
this.setState({message: e.target.value});
|
||||
if (this.props.onKeyPress) {
|
||||
const now = new Date().getTime();
|
||||
if (now - this.state.keypressTimestamp > KEYPRESS_DELAY) {
|
||||
if (now - this.keypressTimestamp > KEYPRESS_DELAY) {
|
||||
this.props.onKeyPress();
|
||||
newState.keypressTimestamp = now;
|
||||
this.keypressTimestamp = now;
|
||||
}
|
||||
}
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
handleQuoteClick(e) {
|
||||
@@ -221,10 +249,10 @@ class SendMessage extends React.PureComponent {
|
||||
<>
|
||||
{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>
|
||||
</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>
|
||||
</a>
|
||||
</>
|
||||
@@ -235,28 +263,28 @@ class SendMessage extends React.PureComponent {
|
||||
(this.state.audioRec ?
|
||||
(<Suspense fallback={<div>Loading...</div>}>
|
||||
<AudioRecorder
|
||||
onRecordingProgress={_ => this.props.onKeyPress(true)}
|
||||
onDeleted={_ => this.setState({audioRec: false})}
|
||||
onFinished={this.handleAttachAudio}/>
|
||||
</Suspense>) :
|
||||
<textarea id="sendMessage" placeholder={prompt}
|
||||
value={this.state.message}
|
||||
onChange={this.handleMessageTyping}
|
||||
value={this.state.message} onChange={this.handleMessageTyping}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
ref={(ref) => {this.messageEditArea = ref;}}
|
||||
autoFocus />)}
|
||||
{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>
|
||||
</a> :
|
||||
!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>
|
||||
</a> :
|
||||
null
|
||||
}
|
||||
<input type="file" ref={ref => {this.attachFile = ref}}
|
||||
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'}} />
|
||||
</>
|
||||
:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Image view with progress bar and a cancel button.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
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
+700
-266
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__);
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ "default": () => (/* binding */ AudioRecorder)
|
||||
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
||||
/* harmony export */ });
|
||||
/* 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 _audio_player_jsx__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./audio-player.jsx */ "./src/widgets/audio-player.jsx");
|
||||
/* 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 webm_duration_fix__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(webm_duration_fix__WEBPACK_IMPORTED_MODULE_2__);
|
||||
/* 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 _lib_strformat__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../lib/strformat */ "./src/lib/strformat.js");
|
||||
/* harmony import */ var _config_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../config.js */ "./src/config.js");
|
||||
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-intl */ "react-intl");
|
||||
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
||||
/* harmony import */ var _audio_player_jsx__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./audio-player.jsx */ "./src/widgets/audio-player.jsx");
|
||||
/* 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 webm_duration_fix__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(webm_duration_fix__WEBPACK_IMPORTED_MODULE_3__);
|
||||
/* 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 MAX_SAMPLES_PER_BAR = 10;
|
||||
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) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -60,6 +93,7 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
||||
this.startedOn = null;
|
||||
this.viewBuffer = [];
|
||||
this.canvasRef = react__WEBPACK_IMPORTED_MODULE_0___default().createRef();
|
||||
this.recordingTimestamp = 0;
|
||||
}
|
||||
componentDidMount() {
|
||||
this.stream = null;
|
||||
@@ -102,16 +136,16 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
||||
window.requestAnimationFrame(drawFrame);
|
||||
const duration = this.durationMillis + (Date.now() - this.startedOn);
|
||||
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.mediaRecorder.pause();
|
||||
this.durationMillis += Date.now() - this.startedOn;
|
||||
this.setState({
|
||||
enabled: 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);
|
||||
@@ -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.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();
|
||||
}
|
||||
@@ -208,8 +247,8 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
||||
this.analyser.fftSize = BUFFER_SIZE;
|
||||
this.audioInput.connect(this.analyser);
|
||||
this.mediaRecorder.onstop = _ => {
|
||||
if (this.durationMillis > _config_js__WEBPACK_IMPORTED_MODULE_5__.MIN_DURATION) {
|
||||
this.getRecording(this.mediaRecorder.mimeType, this.durationMillis).then(result => this.props.onFinished(result.url, result.preview, this.durationMillis));
|
||||
if (this.durationMillis > _config_js__WEBPACK_IMPORTED_MODULE_6__.MIN_DURATION) {
|
||||
this.getRecording(this.mediaRecorder.mimeType).then(result => this.props.onFinished(result.url, result.preview, this.durationMillis));
|
||||
} else {
|
||||
this.props.onDeleted();
|
||||
}
|
||||
@@ -232,18 +271,20 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
||||
this.startedOn = Date.now();
|
||||
this.mediaRecorder.start();
|
||||
this.visualize();
|
||||
this.props.onRecordingProgress();
|
||||
this.recordingTimestamp = this.startedOn;
|
||||
}
|
||||
getRecording(mimeType, duration) {
|
||||
getRecording(mimeType) {
|
||||
mimeType = mimeType || AUDIO_MIME_TYPE;
|
||||
let blob = new Blob(this.audioChunks, {
|
||||
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;
|
||||
return fixedBlob.arrayBuffer();
|
||||
}).then(arrayBuff => this.audioContext.decodeAudioData(arrayBuff)).then(decoded => this.createPreview(decoded)).then(preview => ({
|
||||
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) {
|
||||
@@ -274,18 +315,21 @@ class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
formatMessage
|
||||
} = this.props.intl;
|
||||
const resumeClass = 'material-icons ' + (this.state.enabled ? 'red' : 'gray');
|
||||
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: "audio"
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||
href: "#",
|
||||
onClick: this.handleDelete,
|
||||
title: "Delete"
|
||||
title: formatMessage(messages.icon_title_delete)
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||
className: "material-icons gray"
|
||||
}, "delete_outline")), this.state.recording ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("canvas", {
|
||||
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,
|
||||
preview: this.state.preview,
|
||||
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", {
|
||||
href: "#",
|
||||
onClick: this.handlePause,
|
||||
title: "Pause"
|
||||
title: formatMessage(messages.icon_title_pause)
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||
className: "material-icons"
|
||||
}, "pause_circle_outline")) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||
href: "#",
|
||||
onClick: this.handleResume,
|
||||
title: "Resume"
|
||||
title: formatMessage(messages.icon_title_resume)
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||
className: resumeClass
|
||||
}, "radio_button_checked")), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||
href: "#",
|
||||
onClick: this.handleDone,
|
||||
title: "Send"
|
||||
title: formatMessage(messages.icon_title_send)
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||
className: "material-icons"
|
||||
}, "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