Compare commits

...
25 Commits
Author SHA1 Message Date
or-else 03e87697cf merge with drag-n-drop 2022-12-13 17:38:07 -08:00
or-else bccf0d3b6b video uploading + preview in bubbles works 2022-12-13 17:37:04 -08:00
aforge 36b7d1b721 Merge pull request #104 from tinode/drag-and-drop
Drag & drop for attaching files.
2022-12-13 10:37:32 -08:00
aforge 2294410fb9 Merge branch 'next' into drag-and-drop 2022-12-13 10:36:47 -08:00
aforge 10140c6450 Move file attach DnD to MessagesView. 2022-12-13 00:20:36 -08:00
or-else 489a05663a adding widget for displaying video previes inside message bubbles 2022-12-12 22:09:17 -08:00
or-else f31a3ebdcc .gitattributes + *.min.css binary 2022-12-12 22:06:29 -08:00
or-else 6360708255 working on display of images 2022-12-12 22:05:08 -08:00
or-else 24a87f3deb videos work but with bugs 2022-12-12 18:37:59 -08:00
or-else a8d7e6be50 wip: adding support for video recordings 2022-12-12 16:48:15 -08:00
aforge b434c4ef17 Drag & drop for attaching files. 2022-12-12 16:32:48 -08:00
or-else 0d33c0f0a8 removed unused variables 2022-12-12 13:40:52 -08:00
or-else e95aea17be make date dividers reasier to read 2022-12-11 17:43:07 -08:00
or-else d820da2ee2 build with the SDK fix (timestamps of edited messages) 2022-12-10 18:37:07 -08:00
or-else 692e6e1d8f send "recording" notifications when audio is being recorded 2022-12-10 18:17:03 -08:00
or-else 6aca7e527e removed unused import 2022-12-08 14:07:24 -08:00
or-else ae34795c2d removed legacy translations 2022-12-08 14:05:28 -08:00
or-else 310b352a08 disable editing of some types of messages 2022-12-08 14:03:10 -08:00
or-else b8e9ca8d0a removed support for display of deleted message ranges 2022-12-08 12:32:30 -08:00
or-else 7731c6700b fix for https://github.com/tinode/chat/issues/808 2022-12-07 19:17:18 -08:00
or-else 563f82d265 merge devel 2022-12-07 18:29:28 -08:00
or-else 10b24cc16e updated dependencies, rebuilt, version bumped to 0.20.3 2022-12-06 15:19:27 -08:00
or-else f6cd3048e1 on-demand translation loading works now 2022-12-05 17:18:47 -08:00
or-else 0e11ae9382 rebuilt 2022-12-05 13:58:48 -08:00
or-else fa78e678b9 rebuilt 2022-12-05 09:24:38 -08:00
76 changed files with 2419 additions and 1115 deletions
+1
View File
@@ -1,2 +1,3 @@
umd/* binary
package-lock.json binary
*.min.css binary
+80 -1
View File
@@ -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.
*/
+1 -1
View File
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

+25 -25
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+36 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+8 -13
View File
@@ -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'));
root.render(
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
<TinodeWeb />
</IntlProvider>
);
messageLoader[htmlLang]().then(messages =>
root.render(
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
<TinodeWeb />
</IntlProvider>
));
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+227 -80
View File
@@ -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,10 +755,14 @@ 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()) {
topic.noteKeyPress();
if (audio) {
topic.noteRecording(true);
} else {
topic.noteKeyPress();
}
}
}
@@ -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;
@@ -876,32 +892,34 @@ class MessagesView extends React.Component {
// Convert tiny image into base64 for serialization and previewing.
.then(scaled => blobToBase64(scaled.blob))
.then(b64 => {
let msg = Drafty.insertImage(null, 0, {
mime: mime,
_tempPreview: b64.bits, // This preview will not be serialized.
width: width,
height: height,
filename: fname,
size: blob.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);
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,
size: blob.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, 'err'));
return;
return;
}
// Upload the image if it's too big to be send inband.
// Send the image inband if it's not too big. The image has been scaled already
// in image preview.
blobToBase64(blob)
.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}
</>
);
}
+3 -3
View File
@@ -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}
/>
:
+13 -2
View File
@@ -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);
+48 -8
View File
@@ -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);
+3 -5
View File
@@ -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);
+2 -2
View File
@@ -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}`}>
+10 -10
View File
@@ -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,12 +77,17 @@ 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) {
menuItems.push('menu_item_edit');
let immutable = false;
Drafty.entities(this.props.content, (_0, _1, tp) => {
immutable = ['AU', 'EX', 'FM', 'IM', 'QQ', 'VC', 'VD'].includes(tp);
return immutable;
});
if (!immutable) {
menuItems.push('menu_item_edit');
}
}
}
menuItems.push('menu_item_forward');
@@ -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"
+1 -1
View File
@@ -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);
+84
View File
@@ -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 -1
View File
@@ -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';
+27
View File
@@ -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 -9
View File
@@ -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';
+41 -13
View File
@@ -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
View File
@@ -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';
+60
View File
@@ -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>
);
}
};
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+702 -268
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
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
+65 -20
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long