Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ad723b854 | ||
|
|
4af7a39527 | ||
|
|
a2014826e3 | ||
|
|
3a002f224d | ||
|
|
5fdcc65b5b | ||
|
|
476452c59e | ||
|
|
b0c0790b91 | ||
|
|
9d83dee14c | ||
|
|
09223c9efc | ||
|
|
d0a23b0792 | ||
|
|
d54e4a8153 | ||
|
|
57a062266f | ||
|
|
ed71c6bb6e | ||
|
|
307665d8ec | ||
|
|
297aef7bf1 | ||
|
|
161f544442 | ||
|
|
22e782fa2d | ||
|
|
7135553e42 | ||
|
|
1913ed0c11 | ||
|
|
6de41f4988 | ||
|
|
c5e744aacf | ||
|
|
983de97d7f | ||
|
|
7236c913da | ||
|
|
72ec3b51a0 | ||
|
|
3653453897 |
+1
-7
@@ -1,8 +1,2 @@
|
|||||||
index.dev.js binary
|
umd/* binary
|
||||||
index.dev.js.map binary
|
|
||||||
index.prod.js binary
|
|
||||||
index.prod.js.map binary
|
|
||||||
base.min.css binary
|
|
||||||
tinode.dev.js binary
|
|
||||||
tinode.prod.js binary
|
|
||||||
package-lock.json binary
|
package-lock.json binary
|
||||||
|
|||||||
+2
-1
@@ -949,6 +949,7 @@ i.search {
|
|||||||
/* partial bottom border */
|
/* partial bottom border */
|
||||||
background: linear-gradient(to right, transparent 3.5rem, #eee 1px, #eee 100%) 0 100% / 100% 1px no-repeat, #fff;
|
background: linear-gradient(to right, transparent 3.5rem, #eee 1px, #eee 100%) 0 100% / 100% 1px no-repeat, #fff;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-box.small > li {
|
.contact-box.small > li {
|
||||||
@@ -1944,7 +1945,7 @@ video {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 36rem;
|
max-width: 36rem;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: anywhere;
|
||||||
padding: 0 0.75rem 0 0.75rem;
|
padding: 0 0.75rem 0 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Generated
+874
-906
File diff suppressed because it is too large
Load Diff
+5
-9
@@ -9,6 +9,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run vers && npm run build:css && npm run extract && npm run build:i18n && npm run build:prod && npm run build:dev",
|
"build": "npm run vers && npm run build:css && npm run extract && npm run build:i18n && npm run build:prod && npm run build:dev",
|
||||||
|
"clean": "rm umd/* && rm css/base.min.css && rm i18n.min/*",
|
||||||
"build:css": "postcss css/base.css > css/base.min.css",
|
"build:css": "postcss css/base.css > css/base.min.css",
|
||||||
"build:dev": "webpack --mode development",
|
"build:dev": "webpack --mode development",
|
||||||
"build:prod": "webpack --mode production",
|
"build:prod": "webpack --mode production",
|
||||||
@@ -21,12 +22,8 @@
|
|||||||
},
|
},
|
||||||
"browserslist": "> 0.5%, not IE 11, not op_mini all, not and_uc >0",
|
"browserslist": "> 0.5%, not IE 11, not op_mini all, not and_uc >0",
|
||||||
"files": [
|
"files": [
|
||||||
"umd/index.prod.js",
|
"umd/*.js",
|
||||||
"umd/index.prod.js.map",
|
"umd/*.map",
|
||||||
"umd/index.dev.js",
|
|
||||||
"umd/index.dev.js.map",
|
|
||||||
"umd/tinode.prod.js",
|
|
||||||
"umd/tinode.dev.js",
|
|
||||||
"audio/*.m4a",
|
"audio/*.m4a",
|
||||||
"css/base.css",
|
"css/base.css",
|
||||||
"css/base.min.css",
|
"css/base.min.css",
|
||||||
@@ -68,12 +65,11 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"firebase": "^9.14.0",
|
"firebase": "^9.14.0",
|
||||||
"fix-webm-duration": "^1.0.5",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-intl": "^6.2.1",
|
"react-intl": "^6.2.1",
|
||||||
"tinode-sdk": "^0.21.0-beta1",
|
"tinode-sdk": "^0.20.2",
|
||||||
"webrtc-adapter": "^8.2.0"
|
"webm-duration-fix": "^1.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.2",
|
"@babel/core": "^7.20.2",
|
||||||
|
|||||||
+9
-3
@@ -9,10 +9,10 @@ const mkdirpSync = require('mkdirp').sync;
|
|||||||
|
|
||||||
const EXTRACTED_STRINGS = './src/i18n/ex/base-en.json';
|
const EXTRACTED_STRINGS = './src/i18n/ex/base-en.json';
|
||||||
const BASE_LANG = 'en';
|
const BASE_LANG = 'en';
|
||||||
const LANG_DIR = './src/i18n/';
|
|
||||||
const LANG_PATTERN = './src/i18n/*.json';
|
const LANG_PATTERN = './src/i18n/*.json';
|
||||||
const LANG_PATTERN_ONE = './src/i18n/%s.json';
|
const LANG_PATTERN_ONE = './src/i18n/%s.json';
|
||||||
const FINAL_FILE = './src/messages.json';
|
const FINAL_FILE = './src/messages.json';
|
||||||
|
const OUTPUT_PATTERN = './src/i18n.min/%s.json';
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
@@ -99,8 +99,9 @@ globSync(LANG_PATTERN)
|
|||||||
fs.writeFileSync(filename, JSON.stringify(translated, null, 2)+"\n");
|
fs.writeFileSync(filename, JSON.stringify(translated, null, 2)+"\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Combine all translations into a single json {"lang_1": {...}, "lang_2: {...}"}
|
// Legacy: Combine all translations into a single json {"lang_1": {...}, "lang_2: {...}"}
|
||||||
// while removing messages marked as missing or obsolete.
|
// while removing messages marked as missing or obsolete.
|
||||||
|
// New way: Emit one file per language with all unnecessary data stripped.
|
||||||
|
|
||||||
// Read all translations.
|
// Read all translations.
|
||||||
const messages = globSync(LANG_PATTERN)
|
const messages = globSync(LANG_PATTERN)
|
||||||
@@ -122,6 +123,11 @@ const messages = globSync(LANG_PATTERN)
|
|||||||
return { ...collection, ...descriptors };
|
return { ...collection, ...descriptors };
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// Write a file with all translations combined.
|
// Legacy: Write a single file with all translations combined.
|
||||||
mkdirpSync(path.dirname(FINAL_FILE));
|
mkdirpSync(path.dirname(FINAL_FILE));
|
||||||
fs.writeFileSync(FINAL_FILE, JSON.stringify(messages));
|
fs.writeFileSync(FINAL_FILE, JSON.stringify(messages));
|
||||||
|
|
||||||
|
// New way: write minified ptraslation files.
|
||||||
|
for (let lang in messages) {
|
||||||
|
fs.writeFileSync(printf(OUTPUT_PATTERN, lang), JSON.stringify(messages[lang]));
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@ export const QUOTED_REPLY_LENGTH = 30;
|
|||||||
// Length of a preview of a forwarded message.
|
// Length of a preview of a forwarded message.
|
||||||
export const FORWARDED_PREVIEW_LENGTH = 84;
|
export const FORWARDED_PREVIEW_LENGTH = 84;
|
||||||
// Length of preview for a message being edited.
|
// Length of preview for a message being edited.
|
||||||
export const EDIT_PREVIEW_LENGTH = 60;
|
export const EDIT_PREVIEW_LENGTH = 48;
|
||||||
|
|
||||||
// Minimum duration of an audio recording in milliseconds (2 sec).
|
// Minimum duration of an audio recording in milliseconds (2 sec).
|
||||||
export const MIN_DURATION = 2_000;
|
export const MIN_DURATION = 2_000;
|
||||||
|
|||||||
+25
-8
@@ -10,6 +10,21 @@ import allMessages from './messages.json';
|
|||||||
import TinodeWeb from './views/tinode-web.jsx';
|
import TinodeWeb from './views/tinode-web.jsx';
|
||||||
import HashNavigation from './lib/navigation.js';
|
import HashNavigation from './lib/navigation.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Load just one language.
|
||||||
|
const messageLoader = {
|
||||||
|
'de': _ => import('./i18n.min/de.json'),
|
||||||
|
'en': _ => import('./i18n.min/en.json'),
|
||||||
|
'es': _ => import('./i18n.min/es.json'),
|
||||||
|
'fr': _ => import('./i18n.min/fr.json'),
|
||||||
|
'ko': _ => import('./i18n.min/ko.json'),
|
||||||
|
'ro': _ => import('./i18n.min/ro.json'),
|
||||||
|
'ru': _ => import('./i18n.min/ru.json'),
|
||||||
|
'zh': _ => import('./i18n.min/zh.json'),
|
||||||
|
'zh-TW': _ => import('./i18n.min/zh-TW.json')
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
// Detect human language to use in the UI:
|
// Detect human language to use in the UI:
|
||||||
// Check parameters from URL hash #?hl=ru, then browser, then use 'en' as a fallback.
|
// Check parameters from URL hash #?hl=ru, then browser, then use 'en' as a fallback.
|
||||||
const { params } = HashNavigation.parseUrlHash(window.location.hash);
|
const { params } = HashNavigation.parseUrlHash(window.location.hash);
|
||||||
@@ -19,11 +34,15 @@ const language = (params && params.hl) ||
|
|||||||
navigator.userLanguage ||
|
navigator.userLanguage ||
|
||||||
'en';
|
'en';
|
||||||
|
|
||||||
// Get the base language 'en' from a more specific 'en_GB' or 'en-US' as a partial fallback.
|
// Make sure names like 'en_GB' and 'en-GB' consistently use '-'.
|
||||||
const baseLanguage = language.toLowerCase().split(/[-_]/)[0];
|
const normalized = language.replace('_', '-');
|
||||||
|
// Get the base language 'en' from a more specific 'en-US' as a partial fallback.
|
||||||
|
const baseLanguage = normalized.split('-')[0].toLowerCase();
|
||||||
|
|
||||||
// Try the full locale first, then the locale without the region code, fallback to 'en'.
|
// Try the full locale first, then the locale without the region code, fallback to 'en'.
|
||||||
const htmlLang = allMessages[language] ? language : allMessages[baseLanguage] ? baseLanguage : 'en';
|
// const htmlLang = messageLoader[normalized] ? language : messageLoader[baseLanguage] ? baseLanguage : 'en';
|
||||||
|
// const messages = await messageLoader[htmlLang]();
|
||||||
|
const htmlLang = allMessages[normalized] ? language : allMessages[baseLanguage] ? baseLanguage : 'en';
|
||||||
const messages = allMessages[htmlLang];
|
const messages = allMessages[htmlLang];
|
||||||
|
|
||||||
// Set lang attribute of the HTML element: <html lang="XX">
|
// Set lang attribute of the HTML element: <html lang="XX">
|
||||||
@@ -32,9 +51,7 @@ document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
|
|||||||
// Render the app.
|
// Render the app.
|
||||||
const root = createRoot(document.getElementById('mountPoint'));
|
const root = createRoot(document.getElementById('mountPoint'));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
|
||||||
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
|
<TinodeWeb />
|
||||||
<TinodeWeb />
|
</IntlProvider>
|
||||||
</IntlProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,10 +138,10 @@ export function imageCrop(mime, objURL, left, top, width, height, scale) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.crossOrigin = 'Anonymous';
|
img.crossOrigin = 'Anonymous';
|
||||||
img.onerror = (err) => {
|
img.onerror = _ => {
|
||||||
reject(new Error("Image format unrecognized"));
|
reject(new Error("Image format unrecognized"));
|
||||||
};
|
};
|
||||||
img.onload = () => {
|
img.onload = _ => {
|
||||||
// Once the image is loaded, the URL is no longer needed.
|
// Once the image is loaded, the URL is no longer needed.
|
||||||
URL.revokeObjectURL(img.src);
|
URL.revokeObjectURL(img.src);
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ export function fileToBase64(file) {
|
|||||||
reader.onerror = (evt) => {
|
reader.onerror = (evt) => {
|
||||||
reject(reader.error);
|
reject(reader.error);
|
||||||
};
|
};
|
||||||
reader.onload = () => {
|
reader.onload = _ => {
|
||||||
resolve({mime: file.type, bits: reader.result.split(',')[1], name: file.name});
|
resolve({mime: file.type, bits: reader.result.split(',')[1], name: file.name});
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@@ -189,7 +189,7 @@ export function blobToBase64(blob) {
|
|||||||
reader.onerror = _ => {
|
reader.onerror = _ => {
|
||||||
reject(reader.error);
|
reject(reader.error);
|
||||||
};
|
};
|
||||||
reader.onload = () => {
|
reader.onload = _ => {
|
||||||
resolve({mime: blob.type, bits: reader.result.split(',')[1]});
|
resolve({mime: blob.type, bits: reader.result.split(',')[1]});
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
|
|||||||
@@ -412,8 +412,10 @@ export function replyFormatter(style, data, values, key, stack) {
|
|||||||
try {
|
try {
|
||||||
loadedPromise = cancelablePromise(quoteImage.call(this, data));
|
loadedPromise = cancelablePromise(quoteImage.call(this, data));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Failed to quote image", error);
|
||||||
loadedPromise = cancelablePromise(error);
|
loadedPromise = cancelablePromise(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
attr.whenDone = loadedPromise;
|
attr.whenDone = loadedPromise;
|
||||||
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
|
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
|
||||||
return React.createElement(React.Fragment, {key: key}, values);
|
return React.createElement(React.Fragment, {key: key}, values);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class AccSecurityView extends React.Component {
|
|||||||
this.props.onShowAlert(
|
this.props.onShowAlert(
|
||||||
formatMessage(messages.delete_account), // title
|
formatMessage(messages.delete_account), // title
|
||||||
formatMessage(messages.delete_account_warning), // content
|
formatMessage(messages.delete_account_warning), // content
|
||||||
(() => { this.props.onDeleteAccount(); }), // onConfirm
|
(_ => this.props.onDeleteAccount()), // onConfirm
|
||||||
null, // use default text "OK"
|
null, // use default text "OK"
|
||||||
true, // Show Reject button
|
true, // Show Reject button
|
||||||
null // use default text "Cancel"
|
null // use default text "Cancel"
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default class CreateAccountView extends React.PureComponent {
|
|||||||
// Utility method for converting cropped avatar blob to bytes for sending inband or
|
// Utility method for converting cropped avatar blob to bytes for sending inband or
|
||||||
// for uploading it to the server out of band.
|
// for uploading it to the server out of band.
|
||||||
uploadAvatar(mime, blob, width, height) {
|
uploadAvatar(mime, blob, width, height) {
|
||||||
const readyToUpload = (image) => {
|
const readyToUpload = image => {
|
||||||
let {mime, blob} = image;
|
let {mime, blob} = image;
|
||||||
this.setState({imageUrl: URL.createObjectURL(blob), buttonDisabled: true});
|
this.setState({imageUrl: URL.createObjectURL(blob), buttonDisabled: true});
|
||||||
if (blob.size > MAX_AVATAR_BYTES) {
|
if (blob.size > MAX_AVATAR_BYTES) {
|
||||||
|
|||||||
@@ -410,11 +410,10 @@ class InfoView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isMe = this.props.tinode.isMe(params.topicName);
|
const isMe = this.props.tinode.isMe(params.topicName);
|
||||||
const menuItems = [
|
const menuItems = [{
|
||||||
{title: formatMessage(messages.edit_permissions), handler: () => {
|
title: formatMessage(messages.edit_permissions),
|
||||||
this.handleLaunchPermissionsEditor(isMe ? 'want' : 'user', params.topicName);
|
handler: _ => this.handleLaunchPermissionsEditor(isMe ? 'want' : 'user', params.topicName)
|
||||||
}}
|
}];
|
||||||
];
|
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
menuItems.push('member_delete');
|
menuItems.push('member_delete');
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-15
@@ -400,7 +400,7 @@ class MessagesView extends React.Component {
|
|||||||
|
|
||||||
const setQuery = newTopic ? this.props.newTopicParams : undefined;
|
const setQuery = newTopic ? this.props.newTopicParams : undefined;
|
||||||
topic.subscribe(getQuery.build(), setQuery)
|
topic.subscribe(getQuery.build(), setQuery)
|
||||||
.then((ctrl) => {
|
.then(ctrl => {
|
||||||
if (ctrl.code == 303) {
|
if (ctrl.code == 303) {
|
||||||
// Redirect to another topic requested.
|
// Redirect to another topic requested.
|
||||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', ctrl.params.topic));
|
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', ctrl.params.topic));
|
||||||
@@ -430,7 +430,7 @@ class MessagesView extends React.Component {
|
|||||||
topic.delMessagesList(calls, true);
|
topic.delMessagesList(calls, true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
console.error("Failed subscription to", this.state.topic, err);
|
console.error("Failed subscription to", this.state.topic, err);
|
||||||
this.props.onError(err.message, 'err');
|
this.props.onError(err.message, 'err');
|
||||||
const blankState = MessagesView.getDerivedStateFromProps({}, {});
|
const blankState = MessagesView.getDerivedStateFromProps({}, {});
|
||||||
@@ -447,8 +447,8 @@ class MessagesView extends React.Component {
|
|||||||
const oldTopic = this.props.tinode.getTopic(oldTopicName);
|
const oldTopic = this.props.tinode.getTopic(oldTopicName);
|
||||||
if (oldTopic && oldTopic.isSubscribed()) {
|
if (oldTopic && oldTopic.isSubscribed()) {
|
||||||
oldTopic.leave(false)
|
oldTopic.leave(false)
|
||||||
.catch(() => { /* do nothing here */ })
|
.catch(_ => { /* do nothing here */ })
|
||||||
.finally(() => {
|
.finally(_ => {
|
||||||
// We don't care if the request succeeded or failed.
|
// We don't care if the request succeeded or failed.
|
||||||
// The topic is dead regardless.
|
// The topic is dead regardless.
|
||||||
this.setState({fetchingMessages: false});
|
this.setState({fetchingMessages: false});
|
||||||
@@ -487,10 +487,10 @@ class MessagesView extends React.Component {
|
|||||||
if (event.target.scrollTop <= 0) {
|
if (event.target.scrollTop <= 0) {
|
||||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||||
if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) {
|
if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) {
|
||||||
this.setState({fetchingMessages: true}, () => {
|
this.setState({fetchingMessages: true}, _ => {
|
||||||
topic.getMessagesPage(MESSAGES_PAGE)
|
topic.getMessagesPage(MESSAGES_PAGE)
|
||||||
.catch((err) => this.props.onError(err.message, 'err'))
|
.catch(err => this.props.onError(err.message, 'err'))
|
||||||
.finally(() => this.setState({fetchingMessages: false}));
|
.finally(_ => this.setState({fetchingMessages: false}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -656,9 +656,7 @@ class MessagesView extends React.Component {
|
|||||||
switch (info.what) {
|
switch (info.what) {
|
||||||
case 'kp': {
|
case 'kp': {
|
||||||
clearTimeout(this.keyPressTimer);
|
clearTimeout(this.keyPressTimer);
|
||||||
this.keyPressTimer = setTimeout(() => {
|
this.keyPressTimer = setTimeout(_ => this.setState({typingIndicator: false}), KEYPRESS_DELAY + 1000);
|
||||||
this.setState({typingIndicator: false});
|
|
||||||
}, KEYPRESS_DELAY + 1000);
|
|
||||||
if (!this.state.typingIndicator) {
|
if (!this.state.typingIndicator) {
|
||||||
this.setState({typingIndicator: true});
|
this.setState({typingIndicator: true});
|
||||||
}
|
}
|
||||||
@@ -762,6 +760,11 @@ class MessagesView extends React.Component {
|
|||||||
this.handleCancelReply();
|
this.handleCancelReply();
|
||||||
} else if (this.state.reply) {
|
} else if (this.state.reply) {
|
||||||
if (this.state.reply.editing) {
|
if (this.state.reply.editing) {
|
||||||
|
if (msg == this.state.contentToEdit) {
|
||||||
|
// Message unchanged.
|
||||||
|
this.handleCancelReply();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Editing an existing message.
|
// Editing an existing message.
|
||||||
head = {replace: ':' + this.state.reply.seq};
|
head = {replace: ':' + this.state.reply.seq};
|
||||||
} else if (this.state.reply.content) {
|
} else if (this.state.reply.content) {
|
||||||
@@ -888,9 +891,8 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
// Pass data and the uploader to the TinodeWeb.
|
// Pass data and the uploader to the TinodeWeb.
|
||||||
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||||
}).catch((err) => {
|
})
|
||||||
this.props.onError(err, 'err');
|
.catch(err => this.props.onError(err, 'err'));
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1032,7 +1034,7 @@ class MessagesView extends React.Component {
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
reply: {
|
reply: {
|
||||||
content: Drafty.quote(this.props.intl.formatMessage(messages.message_editing), null, content),
|
content: Drafty.quote(this.props.intl.formatMessage(messages.editing_message), null, content),
|
||||||
seq: seq,
|
seq: seq,
|
||||||
editing: true
|
editing: true
|
||||||
},
|
},
|
||||||
@@ -1199,7 +1201,7 @@ class MessagesView extends React.Component {
|
|||||||
content={msg.content}
|
content={msg.content}
|
||||||
mimeType={msg.head && msg.head.mime}
|
mimeType={msg.head && msg.head.mime}
|
||||||
replyToSeq={replyToSeq}
|
replyToSeq={replyToSeq}
|
||||||
edited={msg.head && msg.head.replace && !msg.head.webrtc}
|
edited={msg.head && !msg.head.webrtc && msg.head.replace}
|
||||||
timestamp={msg.ts}
|
timestamp={msg.ts}
|
||||||
response={isReply}
|
response={isReply}
|
||||||
seq={msg.seq}
|
seq={msg.seq}
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ class NewTopicView extends React.Component {
|
|||||||
{this.state.tabSelected === 'grp' ?
|
{this.state.tabSelected === 'grp' ?
|
||||||
<NewTopicGroup
|
<NewTopicGroup
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
onSubmit={this.handleNewGroupSubmit} /> :
|
onSubmit={this.handleNewGroupSubmit}
|
||||||
|
onError={this.props.onError} /> :
|
||||||
this.state.tabSelected === 'byid' ?
|
this.state.tabSelected === 'byid' ?
|
||||||
<NewTopicById
|
<NewTopicById
|
||||||
onSubmit={this.handleGroupByID}
|
onSubmit={this.handleGroupByID}
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ export default class PasswordResetView extends React.PureComponent {
|
|||||||
} else {
|
} else {
|
||||||
const email = this.state.email.trim();
|
const email = this.state.email.trim();
|
||||||
this.setState({email: email});
|
this.setState({email: email});
|
||||||
this.props.onRequest('email', email).then(() => {
|
this.props.onRequest('email', email).then(_ => this.setState({sent: true}));
|
||||||
this.setState({sent: true});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+74
-105
@@ -303,7 +303,7 @@ class TinodeWeb extends React.Component {
|
|||||||
if (this.state.desktopAlerts) {
|
if (this.state.desktopAlerts) {
|
||||||
this.tinode.setDeviceToken(this.state.firebaseToken);
|
this.tinode.setDeviceToken(this.state.firebaseToken);
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(_ => {
|
||||||
// do nothing: handled earlier.
|
// do nothing: handled earlier.
|
||||||
// catch needed to pervent unnecessary logging of error.
|
// catch needed to pervent unnecessary logging of error.
|
||||||
});
|
});
|
||||||
@@ -322,7 +322,7 @@ class TinodeWeb extends React.Component {
|
|||||||
// When reading from storage, date is returned as string.
|
// When reading from storage, date is returned as string.
|
||||||
token.expires = new Date(token.expires);
|
token.expires = new Date(token.expires);
|
||||||
this.tinode.setAuthToken(token);
|
this.tinode.setAuthToken(token);
|
||||||
this.tinode.connect().catch((err) => {
|
this.tinode.connect().catch(err => {
|
||||||
// Socket error
|
// Socket error
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
@@ -400,7 +400,7 @@ class TinodeWeb extends React.Component {
|
|||||||
// (a) for channels always,
|
// (a) for channels always,
|
||||||
// (b) pushes when the app is in foreground but has no focus.
|
// (b) pushes when the app is in foreground but has no focus.
|
||||||
firebaseOnMessage(this.fcm, (payload) => { this.handlePushMessage(payload); });
|
firebaseOnMessage(this.fcm, (payload) => { this.handlePushMessage(payload); });
|
||||||
}).catch((err) => {
|
}).catch(err => {
|
||||||
// SW registration or FCM has failed :(
|
// SW registration or FCM has failed :(
|
||||||
onError(err);
|
onError(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -454,9 +454,9 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
// Check if a newer version of TinodeWeb app is available at the server.
|
// Check if a newer version of TinodeWeb app is available at the server.
|
||||||
checkForAppUpdate(reg) {
|
checkForAppUpdate(reg) {
|
||||||
reg.onupdatefound = () => {
|
reg.onupdatefound = _ => {
|
||||||
const installingWorker = reg.installing;
|
const installingWorker = reg.installing;
|
||||||
installingWorker.onstatechange = () => {
|
installingWorker.onstatechange = _ => {
|
||||||
if (installingWorker.state == 'installed' && navigator.serviceWorker.controller) {
|
if (installingWorker.state == 'installed' && navigator.serviceWorker.controller) {
|
||||||
const msg = <>
|
const msg = <>
|
||||||
<FormattedMessage id="update_available"
|
<FormattedMessage id="update_available"
|
||||||
@@ -570,7 +570,7 @@ class TinodeWeb extends React.Component {
|
|||||||
if (this.tinode.isConnected()) {
|
if (this.tinode.isConnected()) {
|
||||||
this.doLogin(login, password, null, {meth: this.state.credMethod, resp: this.state.credCode});
|
this.doLogin(login, password, null, {meth: this.state.credMethod, resp: this.state.credCode});
|
||||||
} else {
|
} else {
|
||||||
this.tinode.connect().catch((err) => {
|
this.tinode.connect().catch(err => {
|
||||||
// Socket error
|
// Socket error
|
||||||
this.setState({loginDisabled: false, autoLogin: false, loadSpinnerVisible: false});
|
this.setState({loginDisabled: false, autoLogin: false, loadSpinnerVisible: false});
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
@@ -581,12 +581,12 @@ class TinodeWeb extends React.Component {
|
|||||||
// Enable or disable saving the password and IndexedDB.
|
// Enable or disable saving the password and IndexedDB.
|
||||||
handlePersistenceChange(persist) {
|
handlePersistenceChange(persist) {
|
||||||
if (persist) {
|
if (persist) {
|
||||||
this.tinode.initStorage().then(() => {
|
this.tinode.initStorage().then(_ => {
|
||||||
LocalStorageUtil.setObject('keep-logged-in', true);
|
LocalStorageUtil.setObject('keep-logged-in', true);
|
||||||
this.setState({persist: true});
|
this.setState({persist: true});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.tinode.clearStorage().then(() => {
|
this.tinode.clearStorage().then(_ => {
|
||||||
LocalStorageUtil.setObject('keep-logged-in', false);
|
LocalStorageUtil.setObject('keep-logged-in', false);
|
||||||
this.setState({persist: false});
|
this.setState({persist: false});
|
||||||
});
|
});
|
||||||
@@ -625,7 +625,7 @@ class TinodeWeb extends React.Component {
|
|||||||
prom.then(_ => {
|
prom.then(_ => {
|
||||||
// Reconnected: clear error
|
// Reconnected: clear error
|
||||||
this.handleError();
|
this.handleError();
|
||||||
}).catch((err) => {
|
}).catch(err => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -701,7 +701,7 @@ class TinodeWeb extends React.Component {
|
|||||||
loginPromise = connectionPromise.then(_ => this.tinode.loginToken(token, cred));
|
loginPromise = connectionPromise.then(_ => this.tinode.loginToken(token, cred));
|
||||||
}
|
}
|
||||||
|
|
||||||
loginPromise.then((ctrl) => {
|
loginPromise.then(ctrl => {
|
||||||
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
|
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
|
||||||
this.setState({loadSpinnerVisible: false});
|
this.setState({loadSpinnerVisible: false});
|
||||||
if (cred) {
|
if (cred) {
|
||||||
@@ -711,7 +711,7 @@ class TinodeWeb extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
this.handleLoginSuccessful();
|
this.handleLoginSuccessful();
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch(err => {
|
||||||
// Connection or login failed, report error.
|
// Connection or login failed, report error.
|
||||||
this.setState({
|
this.setState({
|
||||||
loginDisabled: false,
|
loginDisabled: false,
|
||||||
@@ -768,12 +768,12 @@ class TinodeWeb extends React.Component {
|
|||||||
withTags().
|
withTags().
|
||||||
withCred().
|
withCred().
|
||||||
build()
|
build()
|
||||||
).catch((err) => {
|
).catch(err => {
|
||||||
this.tinode.disconnect();
|
this.tinode.disconnect();
|
||||||
localStorage.removeItem('auth-token');
|
localStorage.removeItem('auth-token');
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
HashNavigation.navigateTo('');
|
HashNavigation.navigateTo('');
|
||||||
}).finally(() => {
|
}).finally(_ => {
|
||||||
this.setState({loadSpinnerVisible: false});
|
this.setState({loadSpinnerVisible: false});
|
||||||
});
|
});
|
||||||
let urlHash = HashNavigation.setUrlSidePanel(window.location.hash, 'contacts');
|
let urlHash = HashNavigation.setUrlSidePanel(window.location.hash, 'contacts');
|
||||||
@@ -934,7 +934,7 @@ class TinodeWeb extends React.Component {
|
|||||||
if (fnd.isSubscribed()) {
|
if (fnd.isSubscribed()) {
|
||||||
this.tnFndSubsUpdated();
|
this.tnFndSubsUpdated();
|
||||||
} else {
|
} else {
|
||||||
fnd.subscribe(fnd.startMetaQuery().withSub().build()).catch((err) => {
|
fnd.subscribe(fnd.startMetaQuery().withSub().build()).catch(err => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -957,11 +957,9 @@ class TinodeWeb extends React.Component {
|
|||||||
*/
|
*/
|
||||||
handleSearchContacts(query) {
|
handleSearchContacts(query) {
|
||||||
const fnd = this.tinode.getFndTopic();
|
const fnd = this.tinode.getFndTopic();
|
||||||
fnd.setMeta({desc: {public: query}}).then((ctrl) => {
|
fnd.setMeta({desc: {public: query}})
|
||||||
return fnd.getMeta(fnd.startMetaQuery().withSub().build());
|
.then(_ => fnd.getMeta(fnd.startMetaQuery().withSub().build()))
|
||||||
}).catch((err) => {
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
this.handleError(err.message, 'err');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// User clicked on a topic in the side panel or deleted a topic.
|
// User clicked on a topic in the side panel or deleted a topic.
|
||||||
@@ -1043,7 +1041,7 @@ class TinodeWeb extends React.Component {
|
|||||||
// Topic is not subscribed yet. Subscribe.
|
// Topic is not subscribed yet. Subscribe.
|
||||||
const subscribePromise =
|
const subscribePromise =
|
||||||
topic.subscribe()
|
topic.subscribe()
|
||||||
.then(() => {
|
.then(_ => {
|
||||||
// If there are unsent messages (except video call messages),
|
// If there are unsent messages (except video call messages),
|
||||||
// try sending them now. Unsent video call messages will be dropped.
|
// try sending them now. Unsent video call messages will be dropped.
|
||||||
let calls = [];
|
let calls = [];
|
||||||
@@ -1069,15 +1067,13 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
// TODO: check if return is required.
|
// TODO: check if return is required.
|
||||||
return topic.publishDraft(msg, Promise.all(completion))
|
return topic.publishDraft(msg, Promise.all(completion))
|
||||||
.then((ctrl) => {
|
.then(ctrl => {
|
||||||
if (topic.isArchived()) {
|
if (topic.isArchived()) {
|
||||||
topic.archive(false);
|
topic.archive(false);
|
||||||
}
|
}
|
||||||
return ctrl;
|
return ctrl;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
this.handleError(err.message, 'err');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewChatInvitation(topicName, action) {
|
handleNewChatInvitation(topicName, action) {
|
||||||
@@ -1091,9 +1087,7 @@ class TinodeWeb extends React.Component {
|
|||||||
if (topic.isP2PType()) {
|
if (topic.isP2PType()) {
|
||||||
// For P2P topics change 'given' permission of the peer too.
|
// For P2P topics change 'given' permission of the peer too.
|
||||||
// In p2p topics the other user has the same name as the topic.
|
// In p2p topics the other user has the same name as the topic.
|
||||||
response = response.then((ctrl) => {
|
response = response.then(_ => topic.setMeta({sub: {user: topicName, mode: mode}}));
|
||||||
topic.setMeta({sub: {user: topicName, mode: mode}});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
@@ -1104,18 +1098,14 @@ class TinodeWeb extends React.Component {
|
|||||||
// Ban the topic making futher invites impossible.
|
// Ban the topic making futher invites impossible.
|
||||||
// Just self-ban.
|
// Just self-ban.
|
||||||
const am = topic.getAccessMode().updateWant('-JP').getWant();
|
const am = topic.getAccessMode().updateWant('-JP').getWant();
|
||||||
response = topic.setMeta({sub: {mode: am}}).then((ctrl) => {
|
response = topic.setMeta({sub: {mode: am}}).then(_ => this.handleTopicSelected(null));
|
||||||
return this.handleTopicSelected(null);
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn("Unknown invitation action", '"' + action + '""');
|
console.warn("Unknown invitation action", '"' + action + '""');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
response.catch((err) => {
|
response.catch(err => this.handleError(err.message, 'err'));
|
||||||
this.handleError(err.message, 'err');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1156,7 +1146,7 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
const me = this.tinode.getMeTopic();
|
const me = this.tinode.getMeTopic();
|
||||||
const am = me.getAccessMode().updateWant(on ? '-P' : '+P').getWant();
|
const am = me.getAccessMode().updateWant(on ? '-P' : '+P').getWant();
|
||||||
me.setMeta({sub: {mode: am}}).catch((err) => {
|
me.setMeta({sub: {mode: am}}).catch(err => {
|
||||||
// Request failed, keep existing state.
|
// Request failed, keep existing state.
|
||||||
this.setState({incognitoMode: !on});
|
this.setState({incognitoMode: !on});
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
@@ -1165,9 +1155,7 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
handleUpdateAccountTagsRequest(tags) {
|
handleUpdateAccountTagsRequest(tags) {
|
||||||
this.tinode.getMeTopic().setMeta({tags: tags})
|
this.tinode.getMeTopic().setMeta({tags: tags})
|
||||||
.catch((err) => {
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
this.handleError(err.message, 'err');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// User chose Settings menu item.
|
// User chose Settings menu item.
|
||||||
@@ -1235,9 +1223,9 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.state.firebaseToken && this.fcm) {
|
} else if (this.state.firebaseToken && this.fcm) {
|
||||||
firebaseDelToken(this.fcm).catch((err) => {
|
firebaseDelToken(this.fcm).catch(err => {
|
||||||
console.error("Unable to delete token.", err);
|
console.error("Unable to delete token.", err);
|
||||||
}).finally(() => {
|
}).finally(_ => {
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
||||||
localStorage.removeItem('firebase-token');
|
localStorage.removeItem('firebase-token');
|
||||||
this.setState({desktopAlerts: false, firebaseToken: null});
|
this.setState({desktopAlerts: false, firebaseToken: null});
|
||||||
@@ -1259,16 +1247,14 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
handleCredAdd(method, value) {
|
handleCredAdd(method, value) {
|
||||||
const me = this.tinode.getMeTopic();
|
const me = this.tinode.getMeTopic();
|
||||||
me.setMeta({cred: {meth: method, val: value}}).catch((err) => {
|
me.setMeta({cred: {meth: method, val: value}})
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCredDelete(method, value) {
|
handleCredDelete(method, value) {
|
||||||
const me = this.tinode.getMeTopic();
|
const me = this.tinode.getMeTopic();
|
||||||
me.delCredential(method, value).catch((err) => {
|
me.delCredential(method, value)
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCredConfirm(method, response) {
|
handleCredConfirm(method, response) {
|
||||||
@@ -1332,7 +1318,7 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
params._topicName = topicName;
|
params._topicName = topicName;
|
||||||
this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)});
|
this.setState({newTopicParams: params}, _ => {this.handleTopicSelected(topicName)});
|
||||||
}
|
}
|
||||||
|
|
||||||
// New topic was created, here is the new topic name.
|
// New topic was created, here is the new topic name.
|
||||||
@@ -1377,18 +1363,15 @@ class TinodeWeb extends React.Component {
|
|||||||
if (defacs) {
|
if (defacs) {
|
||||||
params.defacs = defacs;
|
params.defacs = defacs;
|
||||||
}
|
}
|
||||||
topic.setMeta({desc: params, attachments: attachments}).catch((err) => {
|
topic.setMeta({desc: params, attachments: attachments})
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnarchive(topicName) {
|
handleUnarchive(topicName) {
|
||||||
const topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
if (topic) {
|
if (topic) {
|
||||||
topic.archive(false).catch((err) => {
|
topic.archive(false).catch(err => this.handleError(err.message, 'err'));
|
||||||
this.handleError(err.message, 'err');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1396,9 +1379,8 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleError();
|
this.handleError();
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
this.tinode.updateAccountBasic(null, this.tinode.getCurrentLogin(), password).catch((err) => {
|
this.tinode.updateAccountBasic(null, this.tinode.getCurrentLogin(), password)
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1413,18 +1395,16 @@ class TinodeWeb extends React.Component {
|
|||||||
am.updateWant(mode);
|
am.updateWant(mode);
|
||||||
mode = am.getWant();
|
mode = am.getWant();
|
||||||
}
|
}
|
||||||
topic.setMeta({sub: {user: uid, mode: mode}}).catch((err) => {
|
topic.setMeta({sub: {user: uid, mode: mode}})
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagsUpdateRequest(topicName, tags) {
|
handleTagsUpdateRequest(topicName, tags) {
|
||||||
const topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
if (topic) {
|
if (topic) {
|
||||||
topic.setMeta({tags: tags}).catch((err) => {
|
topic.setMeta({tags: tags})
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1451,9 +1431,9 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
this.setState(this.getBlankState());
|
this.setState(this.getBlankState());
|
||||||
|
|
||||||
cleared.then(() => {
|
cleared.then(_ => {
|
||||||
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, isSecureConnection(),
|
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, isSecureConnection(),
|
||||||
this.state.transport, this.props.intl.locale, LocalStorageUtil.getObject('keep-logged-in'), () => {
|
this.state.transport, this.props.intl.locale, LocalStorageUtil.getObject('keep-logged-in'), _ => {
|
||||||
this.tinode.onConnect = this.handleConnected;
|
this.tinode.onConnect = this.handleConnected;
|
||||||
this.tinode.onDisconnect = this.handleDisconnect;
|
this.tinode.onDisconnect = this.handleDisconnect;
|
||||||
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
|
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
|
||||||
@@ -1477,10 +1457,10 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Request to hard-delete topic.
|
// Request to hard-delete topic.
|
||||||
topic.delTopic(true).then((ctrl) => {
|
topic.delTopic(true).then(_ => {
|
||||||
// Hide MessagesView and InfoView panels.
|
// Hide MessagesView and InfoView panels.
|
||||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||||
}).catch((err) => {
|
}).catch(err => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1492,9 +1472,8 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Request hard-delete all messages.
|
// Request hard-delete all messages.
|
||||||
topic.delMessagesAll(true).catch((err) => {
|
topic.delMessagesAll(true)
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLeaveUnsubRequest(topicName) {
|
handleLeaveUnsubRequest(topicName) {
|
||||||
@@ -1503,10 +1482,10 @@ class TinodeWeb extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
topic.leave(true).then((ctrl) => {
|
topic.leave(true).then(_ => {
|
||||||
// Hide MessagesView and InfoView panels.
|
// Hide MessagesView and InfoView panels.
|
||||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||||
}).catch((err) => {
|
}).catch(err => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1517,12 +1496,12 @@ class TinodeWeb extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
topic.updateMode(null, '-JP').then(_ => {
|
topic.updateMode(null, '-JP')
|
||||||
// Hide MessagesView and InfoView panels.
|
.then(_ => {
|
||||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
// Hide MessagesView and InfoView panels.
|
||||||
}).catch((err) => {
|
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||||
this.handleError(err.message, 'err');
|
})
|
||||||
});
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReportTopic(topicName) {
|
handleReportTopic(topicName) {
|
||||||
@@ -1535,12 +1514,12 @@ class TinodeWeb extends React.Component {
|
|||||||
this.tinode.report('report', topicName);
|
this.tinode.report('report', topicName);
|
||||||
|
|
||||||
// Remove J and P permissions.
|
// Remove J and P permissions.
|
||||||
topic.updateMode(null, '-JP').then((ctrl) => {
|
topic.updateMode(null, '-JP')
|
||||||
|
.then(_ => {
|
||||||
// Hide MessagesView and InfoView panels.
|
// Hide MessagesView and InfoView panels.
|
||||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||||
}).catch((err) => {
|
})
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowContextMenu(params, menuItems) {
|
handleShowContextMenu(params, menuItems) {
|
||||||
@@ -1637,7 +1616,7 @@ class TinodeWeb extends React.Component {
|
|||||||
handleContextMenuAction(action, promise, params) {
|
handleContextMenuAction(action, promise, params) {
|
||||||
if (action == 'topic_archive') {
|
if (action == 'topic_archive') {
|
||||||
if (promise && params.topicName && params.topicName == this.state.topicSelected) {
|
if (promise && params.topicName && params.topicName == this.state.topicSelected) {
|
||||||
promise.then(() => {
|
promise.then(_ => {
|
||||||
this.handleTopicSelected(null);
|
this.handleTopicSelected(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1677,17 +1656,15 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
if (added && added.length > 0) {
|
if (added && added.length > 0) {
|
||||||
added.map((uid) => {
|
added.map((uid) => {
|
||||||
topic.invite(uid, null).catch((err) => {
|
topic.invite(uid, null)
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removed && removed.length > 0) {
|
if (removed && removed.length > 0) {
|
||||||
removed.map((uid) => {
|
removed.map((uid) => {
|
||||||
topic.delSubscription(uid).catch((err) => {
|
topic.delSubscription(uid)
|
||||||
this.handleError(err.message, 'err');
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1696,9 +1673,7 @@ class TinodeWeb extends React.Component {
|
|||||||
if (this.tinode.isAuthenticated()) {
|
if (this.tinode.isAuthenticated()) {
|
||||||
// Adding new email or phone number in account setting.
|
// Adding new email or phone number in account setting.
|
||||||
this.tinode.getMeTopic().setMeta({cred: {meth: cred, resp: code}})
|
this.tinode.getMeTopic().setMeta({cred: {meth: cred, resp: code}})
|
||||||
.catch((err) => {
|
.catch(err => this.handleError(err.message, 'err'));
|
||||||
this.handleError(err.message, 'err');
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Credential validation on signup.
|
// Credential validation on signup.
|
||||||
this.setState({credMethod: cred, credCode: code, credToken: token});
|
this.setState({credMethod: cred, credCode: code, credToken: token});
|
||||||
@@ -1709,10 +1684,8 @@ class TinodeWeb extends React.Component {
|
|||||||
handlePasswordResetRequest(method, value) {
|
handlePasswordResetRequest(method, value) {
|
||||||
// If already connected, connnect() will return a resolved promise.
|
// If already connected, connnect() will return a resolved promise.
|
||||||
return this.tinode.connect()
|
return this.tinode.connect()
|
||||||
.then(() => {
|
.then(_ => this.tinode.requestResetAuthSecret('basic', method, value))
|
||||||
return this.tinode.requestResetAuthSecret('basic', method, value);
|
.catch(err => {
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// Socket error
|
// Socket error
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
@@ -1724,13 +1697,9 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleError(this.props.intl.formatMessage(messages.invalid_security_token), 'err');
|
this.handleError(this.props.intl.formatMessage(messages.invalid_security_token), 'err');
|
||||||
} else {
|
} else {
|
||||||
this.tinode.connect()
|
this.tinode.connect()
|
||||||
.then(() => {
|
.then(_ => this.tinode.updateAccountBasic(null, null, newPassword, {token: token}))
|
||||||
return this.tinode.updateAccountBasic(null, null, newPassword, {token: token});
|
.then(_ => HashNavigation.navigateTo(''))
|
||||||
})
|
.catch(err => {
|
||||||
.then(() => {
|
|
||||||
HashNavigation.navigateTo('');
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// Socket error
|
// Socket error
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
@@ -1749,7 +1718,7 @@ class TinodeWeb extends React.Component {
|
|||||||
case CALL_STATE_OUTGOING_INITATED:
|
case CALL_STATE_OUTGOING_INITATED:
|
||||||
let head = { webrtc: CALL_HEAD_STARTED };
|
let head = { webrtc: CALL_HEAD_STARTED };
|
||||||
this.handleSendMessage(Drafty.videoCall(), undefined, undefined, head)
|
this.handleSendMessage(Drafty.videoCall(), undefined, undefined, head)
|
||||||
.then((ctrl) => {
|
.then(ctrl => {
|
||||||
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
|
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
|
||||||
this.handleCallClose();
|
this.handleCallClose();
|
||||||
return;
|
return;
|
||||||
@@ -1836,7 +1805,7 @@ class TinodeWeb extends React.Component {
|
|||||||
// We need to switch and subscribe to callTopic first.
|
// We need to switch and subscribe to callTopic first.
|
||||||
this.setState({
|
this.setState({
|
||||||
callShouldStart: true,
|
callShouldStart: true,
|
||||||
}, () => this.handleTopicSelected(this.state.callTopic));
|
}, _ => this.handleTopicSelected(this.state.callTopic));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1955,9 +1924,9 @@ class TinodeWeb extends React.Component {
|
|||||||
visible={this.state.alertVisible}
|
visible={this.state.alertVisible}
|
||||||
title={this.state.alertParams.title}
|
title={this.state.alertParams.title}
|
||||||
content={this.state.alertParams.content}
|
content={this.state.alertParams.content}
|
||||||
onReject={this.state.alertParams.onReject ? (() => { this.setState({alertVisible: false}); }) : null}
|
onReject={this.state.alertParams.onReject ? (_ => this.setState({alertVisible: false})) : null}
|
||||||
reject={this.state.alertParams.reject}
|
reject={this.state.alertParams.reject}
|
||||||
onConfirm={() => { this.setState({alertVisible: false}); this.state.alertParams.onConfirm(); }}
|
onConfirm={_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}}
|
||||||
confirm={this.state.alertParams.confirm}
|
confirm={this.state.alertParams.confirm}
|
||||||
/>
|
/>
|
||||||
<SidepanelView
|
<SidepanelView
|
||||||
|
|||||||
@@ -27,15 +27,11 @@ export default class Attachment extends React.Component {
|
|||||||
const downloader = this.props.tinode.getLargeFileHelper();
|
const downloader = this.props.tinode.getLargeFileHelper();
|
||||||
this.setState({downloader: downloader});
|
this.setState({downloader: downloader});
|
||||||
downloader.download(url, filename, mimetype,
|
downloader.download(url, filename, mimetype,
|
||||||
(loaded) => {
|
loaded => this.setState({progress: loaded / this.props.size}),
|
||||||
this.setState({progress: loaded / this.props.size});
|
err => this.props.onError(err, 'err')
|
||||||
},
|
)
|
||||||
(err) => {
|
.then(_ => this.setState({downloader: null, progress: 0}))
|
||||||
this.props.onError(err, 'err');
|
.catch(err => {
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
this.setState({downloader: null, progress: 0});
|
|
||||||
}).catch((err) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
this.props.onError("Error downloading file: " + err.message, 'err');
|
this.props.onError("Error downloading file: " + err.message, 'err');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
|
|
||||||
this.canvasContext.lineWidth = LINE_WIDTH;
|
this.canvasContext.lineWidth = LINE_WIDTH;
|
||||||
|
|
||||||
const drawFrame = () => {
|
const drawFrame = _ => {
|
||||||
if (!this.canvasRef.current || !this.audioPlayer) {
|
if (!this.canvasRef.current || !this.audioPlayer) {
|
||||||
// The component is unmounted.
|
// The component is unmounted.
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import React from 'react';
|
|||||||
|
|
||||||
import AudioPlayer from './audio-player.jsx';
|
import AudioPlayer from './audio-player.jsx';
|
||||||
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=642012
|
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=642012
|
||||||
import fixWebmDuration from 'fix-webm-duration';
|
// It adds duration and SeekHead to the webm record.
|
||||||
|
import fixWebmDuration from 'webm-duration-fix';
|
||||||
|
|
||||||
import { intArrayToBase64 } from '../lib/blob-helpers.js'
|
import { intArrayToBase64 } from '../lib/blob-helpers.js'
|
||||||
import { secondsToTime } from '../lib/strformat';
|
import { secondsToTime } from '../lib/strformat';
|
||||||
@@ -105,7 +106,7 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
let prevBarCount = 0;
|
let prevBarCount = 0;
|
||||||
let volume = 0.0;
|
let volume = 0.0;
|
||||||
let countPerBar = 0;
|
let countPerBar = 0;
|
||||||
const drawFrame = () => {
|
const drawFrame = _ => {
|
||||||
if (!this.startedOn) {
|
if (!this.startedOn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -226,7 +227,7 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
|
|
||||||
initMediaRecording(stream) {
|
initMediaRecording(stream) {
|
||||||
this.stream = stream;
|
this.stream = stream;
|
||||||
this.mediaRecorder = new MediaRecorder(stream, {mimeType: AUDIO_MIME_TYPE});
|
this.mediaRecorder = new MediaRecorder(stream, {mimeType: AUDIO_MIME_TYPE, audioBitsPerSecond: 24_000});
|
||||||
|
|
||||||
// The following code is needed for visualization.
|
// The following code is needed for visualization.
|
||||||
this.audioContext = new AudioContext();
|
this.audioContext = new AudioContext();
|
||||||
@@ -269,8 +270,9 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
// Obtain data in a form sutable for sending or playing back.
|
// Obtain data in a form sutable for sending or playing back.
|
||||||
// If duration is valid, apply fix for Chrome's WebM duration bug.
|
// If duration is valid, apply fix for Chrome's WebM duration bug.
|
||||||
getRecording(mimeType, duration) {
|
getRecording(mimeType, duration) {
|
||||||
let blob = new Blob(this.audioChunks, {type: mimeType || AUDIO_MIME_TYPE});
|
mimeType = mimeType || AUDIO_MIME_TYPE;
|
||||||
return (duration > 0 ? fixWebmDuration(blob, duration, {logger: false}) : Promise.resolve(blob))
|
let blob = new Blob(this.audioChunks, {type: mimeType});
|
||||||
|
return fixWebmDuration(blob, mimeType)
|
||||||
.then(fixedBlob => { blob = fixedBlob; return fixedBlob.arrayBuffer(); })
|
.then(fixedBlob => { blob = fixedBlob; return fixedBlob.arrayBuffer(); })
|
||||||
.then(arrayBuff => this.audioContext.decodeAudioData(arrayBuff))
|
.then(arrayBuff => this.audioContext.decodeAudioData(arrayBuff))
|
||||||
.then(decoded => this.createPreview(decoded))
|
.then(decoded => this.createPreview(decoded))
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ class CallPanel extends React.PureComponent {
|
|||||||
const desc = new RTCSessionDescription(info.payload);
|
const desc = new RTCSessionDescription(info.payload);
|
||||||
this.state.pc.setRemoteDescription(desc)
|
this.state.pc.setRemoteDescription(desc)
|
||||||
.then(_ => {
|
.then(_ => {
|
||||||
this.setState({ callInitialSetupComplete: true }, () => this.drainRemoteIceCandidatesCache());
|
this.setState({ callInitialSetupComplete: true }, _ => this.drainRemoteIceCandidatesCache());
|
||||||
})
|
})
|
||||||
.catch(this.reportError);
|
.catch(this.reportError);
|
||||||
}
|
}
|
||||||
@@ -357,7 +357,7 @@ class CallPanel extends React.PureComponent {
|
|||||||
})
|
})
|
||||||
.then(_ => {
|
.then(_ => {
|
||||||
this.props.onSendAnswer(this.props.topic, this.props.seq, pc.localDescription.toJSON());
|
this.props.onSendAnswer(this.props.topic, this.props.seq, pc.localDescription.toJSON());
|
||||||
this.setState({ callInitialSetupComplete: true }, () => this.drainRemoteIceCandidatesCache());
|
this.setState({ callInitialSetupComplete: true }, _ => this.drainRemoteIceCandidatesCache());
|
||||||
})
|
})
|
||||||
.catch(this.handleGetUserMediaError);
|
.catch(this.handleGetUserMediaError);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
|
|
||||||
this.props.showContextMenu({
|
this.props.showContextMenu({
|
||||||
seq: this.props.seq,
|
seq: this.props.seq,
|
||||||
|
replace: this.props.edited ? parseInt(this.props.edited.split(':')[1]) : 0,
|
||||||
content: this.props.content,
|
content: this.props.content,
|
||||||
userFrom: this.props.userFrom,
|
userFrom: this.props.userFrom,
|
||||||
userName: this.props.userName,
|
userName: this.props.userName,
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class ContextMenu extends React.Component {
|
|||||||
'menu_item_forward': {
|
'menu_item_forward': {
|
||||||
id: 'menu_item_forward',
|
id: 'menu_item_forward',
|
||||||
title: formatMessage(messages.forward),
|
title: formatMessage(messages.forward),
|
||||||
handler: () => {} /* the action is taken directly in tinode-web */
|
handler: _ => {} /* the action is taken directly in tinode-web */
|
||||||
},
|
},
|
||||||
'menu_item_edit': {
|
'menu_item_edit': {
|
||||||
id: 'menu_item_edit',
|
id: 'menu_item_edit',
|
||||||
@@ -227,12 +227,10 @@ class ContextMenu extends React.Component {
|
|||||||
return props.onShowAlert(
|
return props.onShowAlert(
|
||||||
formatMessage(messages.block), // title
|
formatMessage(messages.block), // title
|
||||||
formatMessage(messages.topic_block_warning), // content
|
formatMessage(messages.topic_block_warning), // content
|
||||||
(() => {
|
(_ => this.topicPermissionSetter('-JP', params, errorHandler).then(ctrl => {
|
||||||
return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => {
|
this.props.onTopicRemoved(params.topicName);
|
||||||
this.props.onTopicRemoved(params.topicName);
|
return ctrl;
|
||||||
return ctrl;
|
})),
|
||||||
});
|
|
||||||
}),
|
|
||||||
null, // "OK"
|
null, // "OK"
|
||||||
true, // Show Reject button
|
true, // Show Reject button
|
||||||
null // "Cancel"
|
null // "Cancel"
|
||||||
@@ -246,13 +244,13 @@ class ContextMenu extends React.Component {
|
|||||||
return props.onShowAlert(
|
return props.onShowAlert(
|
||||||
formatMessage(messages.topic_delete), // title
|
formatMessage(messages.topic_delete), // title
|
||||||
formatMessage(messages.topic_delete_warning), // content
|
formatMessage(messages.topic_delete_warning), // content
|
||||||
(() => {
|
(_ => {
|
||||||
const topic = this.props.tinode.getTopic(params.topicName);
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
console.warn("Topic not found: ", params.topicName);
|
console.warn("Topic not found: ", params.topicName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return topic.delTopic(true).catch((err) => {
|
return topic.delTopic(true).catch(err => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
@@ -273,7 +271,7 @@ class ContextMenu extends React.Component {
|
|||||||
console.warn("Topic not found: ", params.topicName);
|
console.warn("Topic not found: ", params.topicName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return topic.archive(true).catch((err) => {
|
return topic.archive(true).catch(err => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
@@ -289,7 +287,7 @@ class ContextMenu extends React.Component {
|
|||||||
console.warn("Topic not found: ", params.topicName);
|
console.warn("Topic not found: ", params.topicName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return topic.archive(false).catch((err) => {
|
return topic.archive(false).catch(err => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
@@ -311,7 +309,7 @@ class ContextMenu extends React.Component {
|
|||||||
console.warn("Topic or user not found: '" + params.topicName + "', '" + params.user + "'");
|
console.warn("Topic or user not found: '" + params.topicName + "', '" + params.user + "'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return topic.delSubscription(params.user).catch((err) => {
|
return topic.delSubscription(params.user).catch(err => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
@@ -401,12 +399,14 @@ class ContextMenu extends React.Component {
|
|||||||
if (!all && topic.cancelSend(params.seq)) {
|
if (!all && topic.cancelSend(params.seq)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Can't cancel. Delete instead.
|
|
||||||
const promise = all ?
|
const promise = all ?
|
||||||
topic.delMessagesAll(hard) :
|
topic.delMessagesAll(hard) :
|
||||||
topic.delMessagesList([params.seq], hard);
|
params.replace > 0 ?
|
||||||
|
topic.delMessagesEdits(params.replace, hard) :
|
||||||
|
topic.delMessagesList([params.seq], hard);
|
||||||
|
|
||||||
return promise.catch((err) => {
|
return promise.catch(err => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
@@ -421,7 +421,7 @@ class ContextMenu extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const msg = topic.createMessage(params.content, false);
|
const msg = topic.createMessage(params.content, false);
|
||||||
return topic.publishDraft(msg).catch((err) => {
|
return topic.publishDraft(msg).catch(err => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
@@ -438,9 +438,7 @@ class ContextMenu extends React.Component {
|
|||||||
|
|
||||||
let result = topic.updateMode(params.user, mode);
|
let result = topic.updateMode(params.user, mode);
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
result = result.catch((err) => {
|
result = result.catch(err => errorHandler(err.message, 'err'));
|
||||||
errorHandler(err.message, 'err');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -450,7 +448,7 @@ class ContextMenu extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editMessage(params, errorHandler) {
|
editMessage(params, errorHandler) {
|
||||||
params.editMessage(params.seq, params.content, errorHandler);
|
params.editMessage(params.replace || params.seq, params.content, errorHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default class InPlaceEdit extends React.Component {
|
|||||||
|
|
||||||
handleStartEditing() {
|
handleStartEditing() {
|
||||||
if (!this.props.readOnly) {
|
if (!this.props.readOnly) {
|
||||||
this.setState({active: true}, () => {
|
this.setState({active: true}, _ => {
|
||||||
if (this.selfRef.current) {
|
if (this.selfRef.current) {
|
||||||
this.selfRef.current.focus();
|
this.selfRef.current.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Image with a placeholder which is replaced when the promise is resolved.
|
// Image with a placeholder which is replaced when the promise is resolved.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
export default class LazyImage extends React.PureComponent {
|
export default class LazyImage extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -20,7 +19,7 @@ export default class LazyImage extends React.PureComponent {
|
|||||||
this.props.whenDone
|
this.props.whenDone
|
||||||
.promise
|
.promise
|
||||||
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
||||||
.catch(() => this.setState({src: 'img/broken_image.png'}));
|
.catch(_ => this.setState({src: 'img/broken_image.png'}));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -33,7 +32,7 @@ export default class LazyImage extends React.PureComponent {
|
|||||||
this.props.whenDone
|
this.props.whenDone
|
||||||
.promise
|
.promise
|
||||||
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
||||||
.catch(() => this.setState({src: 'img/broken_image.png'}));
|
.catch(_ => this.setState({src: 'img/broken_image.png'}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import AvatarCrop from './avatar-crop.jsx';
|
||||||
import AvatarUpload from './avatar-upload.jsx';
|
import AvatarUpload from './avatar-upload.jsx';
|
||||||
import CheckBox from './checkbox.jsx';
|
import CheckBox from './checkbox.jsx';
|
||||||
import TagManager from './tag-manager.jsx';
|
import TagManager from './tag-manager.jsx';
|
||||||
|
|
||||||
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
|
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE, MAX_TITLE_LENGTH,
|
||||||
|
MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
|
||||||
|
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
export default class NewTopicGroup extends React.PureComponent {
|
export default class NewTopicGroup extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -17,13 +20,18 @@ export default class NewTopicGroup extends React.PureComponent {
|
|||||||
fullName: '', // full/formatted name
|
fullName: '', // full/formatted name
|
||||||
private: '',
|
private: '',
|
||||||
description: '',
|
description: '',
|
||||||
imageDataUrl: null,
|
imageUrl: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
isChannel: false
|
isChannel: false,
|
||||||
|
newAvatar: null,
|
||||||
|
newAvatarMime: null
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleFieldEdit = this.handleFieldEdit.bind(this);
|
this.handleFieldEdit = this.handleFieldEdit.bind(this);
|
||||||
this.handleImageChanged = this.handleImageChanged.bind(this);
|
this.handleImageChanged = this.handleImageChanged.bind(this);
|
||||||
|
this.handleAvatarCropped = this.handleAvatarCropped.bind(this);
|
||||||
|
this.handleAvatarCropCancel = this.handleAvatarCropCancel.bind(this);
|
||||||
|
this.uploadAvatar = this.uploadAvatar.bind(this);
|
||||||
this.handleTagsChanged = this.handleTagsChanged.bind(this);
|
this.handleTagsChanged = this.handleTagsChanged.bind(this);
|
||||||
this.handleChannelToggle = this.handleChannelToggle.bind(this);
|
this.handleChannelToggle = this.handleChannelToggle.bind(this);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
@@ -38,7 +46,47 @@ export default class NewTopicGroup extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleImageChanged(mime, img) {
|
handleImageChanged(mime, img) {
|
||||||
this.setState({imageDataUrl: img});
|
this.setState({newAvatar: img, newAvatarMime: mime});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAvatarCropped(mime, blob, width, height) {
|
||||||
|
const url = blob ? URL.createObjectURL(blob) : null;
|
||||||
|
this.setState({imageUrl: url, newAvatar: null, newAvatarMime: null});
|
||||||
|
if (blob) {
|
||||||
|
this.uploadAvatar(mime, blob, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAvatarCropCancel() {
|
||||||
|
this.setState({newAvatar: null, newAvatarMime: null});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method for converting cropped avatar blob to bytes for sending inband or
|
||||||
|
// for uploading it to the server out of band.
|
||||||
|
uploadAvatar(mime, blob, width, height) {
|
||||||
|
const readyToUpload = image => {
|
||||||
|
let {mime, blob} = image;
|
||||||
|
if (blob.size > MAX_AVATAR_BYTES) {
|
||||||
|
// Too large to send inband - uploading out of band and sending as a link.
|
||||||
|
const uploader = this.props.tinode.getLargeFileHelper();
|
||||||
|
uploader.upload(blob)
|
||||||
|
.then(url => this.setState({imageUrl: url}))
|
||||||
|
.catch(err => this.props.onError(err.message, 'err'));
|
||||||
|
} else {
|
||||||
|
// Convert blob to base64-encoded bits.
|
||||||
|
blobToBase64(blob)
|
||||||
|
.then(b64 => this.setState({imageUrl: makeImageUrl({data: b64.bits, type: mime})}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (width > AVATAR_SIZE || height > AVATAR_SIZE || width != height) {
|
||||||
|
// Avatar is not square or too large even after cropping. Shrink it and make square.
|
||||||
|
imageScaled(blob, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true)
|
||||||
|
.then(scaled => readyToUpload(scaled))
|
||||||
|
.catch(err => this.props.onError(err.message, 'err'));
|
||||||
|
} else {
|
||||||
|
readyToUpload({mime: mime, blob: blob, width: width, height: height});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagsChanged(tags) {
|
handleTagsChanged(tags) {
|
||||||
@@ -56,11 +104,22 @@ export default class NewTopicGroup extends React.PureComponent {
|
|||||||
const comment = this.state.private.trim().substring(0, MAX_TITLE_LENGTH);
|
const comment = this.state.private.trim().substring(0, MAX_TITLE_LENGTH);
|
||||||
const description = this.state.description.trim().substring(0, MAX_TOPIC_DESCRIPTION_LENGTH);
|
const description = this.state.description.trim().substring(0, MAX_TOPIC_DESCRIPTION_LENGTH);
|
||||||
if (fn) {
|
if (fn) {
|
||||||
this.props.onSubmit(fn, description, this.state.imageDataUrl, comment, this.state.tags, this.state.isChannel);
|
this.props.onSubmit(fn, description, this.state.imageUrl, comment, this.state.tags, this.state.isChannel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (this.state.newAvatar) {
|
||||||
|
return (
|
||||||
|
<AvatarCrop
|
||||||
|
avatar={this.state.newAvatar}
|
||||||
|
mime={this.state.newAvatarMime}
|
||||||
|
onSubmit={this.handleAvatarCropped}
|
||||||
|
onCancel={this.handleAvatarCropCancel}
|
||||||
|
onError={this.props.onError} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let submitClasses = 'primary';
|
let submitClasses = 'primary';
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
submitClasses += ' disabled';
|
submitClasses += ' disabled';
|
||||||
@@ -71,6 +130,7 @@ export default class NewTopicGroup extends React.PureComponent {
|
|||||||
<center>
|
<center>
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
|
avatar={this.state.imageUrl}
|
||||||
onError={this.props.onError}
|
onError={this.props.onError}
|
||||||
onImageUpdated={this.handleImageChanged} />
|
onImageUpdated={this.handleImageChanged} />
|
||||||
</center>
|
</center>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// Send message form.
|
// Send message form.
|
||||||
import React from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { Drafty } from 'tinode-sdk';
|
import { Drafty } from 'tinode-sdk';
|
||||||
|
|
||||||
import AudioRecorder from './audio-recorder.jsx';
|
// Lazy-loading AudioRecorder because it's quite large due to
|
||||||
|
// a dependency on webm-duration-fix.
|
||||||
|
const AudioRecorder = React.lazy(_ => import('./audio-recorder.jsx'));
|
||||||
|
|
||||||
import { KEYPRESS_DELAY } from '../config.js';
|
import { KEYPRESS_DELAY } from '../config.js';
|
||||||
import { filePasted } from '../lib/blob-helpers.js';
|
import { filePasted } from '../lib/blob-helpers.js';
|
||||||
@@ -231,9 +233,11 @@ class SendMessage extends React.PureComponent {
|
|||||||
{this.props.noInput ?
|
{this.props.noInput ?
|
||||||
(quote || <div className="hr thin" />) :
|
(quote || <div className="hr thin" />) :
|
||||||
(this.state.audioRec ?
|
(this.state.audioRec ?
|
||||||
<AudioRecorder
|
(<Suspense fallback={<div>Loading...</div>}>
|
||||||
onDeleted={_ => this.setState({audioRec: false})}
|
<AudioRecorder
|
||||||
onFinished={this.handleAttachAudio}/> :
|
onDeleted={_ => this.setState({audioRec: false})}
|
||||||
|
onFinished={this.handleAttachAudio}/>
|
||||||
|
</Suspense>) :
|
||||||
<textarea id="sendMessage" placeholder={prompt}
|
<textarea id="sendMessage" placeholder={prompt}
|
||||||
value={this.state.message}
|
value={this.state.message}
|
||||||
onChange={this.handleMessageTyping}
|
onChange={this.handleMessageTyping}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export default class TopicDescEdit extends React.Component {
|
|||||||
// Utility method for converting cropped avatar blob to bytes for sending inband or
|
// Utility method for converting cropped avatar blob to bytes for sending inband or
|
||||||
// for uploading it to the server out of band.
|
// for uploading it to the server out of band.
|
||||||
uploadAvatar(mime, blob, width, height) {
|
uploadAvatar(mime, blob, width, height) {
|
||||||
const readyToUpload = (image) => {
|
const readyToUpload = image => {
|
||||||
let {mime, blob} = image;
|
let {mime, blob} = image;
|
||||||
if (blob.size > MAX_AVATAR_BYTES) {
|
if (blob.size > MAX_AVATAR_BYTES) {
|
||||||
// Too large to send inband - uploading out of band and sending as a link.
|
// Too large to send inband - uploading out of band and sending as a link.
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class TopicSecurity extends React.PureComponent {
|
|||||||
this.props.onShowAlert(
|
this.props.onShowAlert(
|
||||||
formatMessage(messages.topic_delete), // title
|
formatMessage(messages.topic_delete), // title
|
||||||
formatMessage(messages.topic_delete_warning), // content
|
formatMessage(messages.topic_delete_warning), // content
|
||||||
(() => { this.props.onDeleteTopic(this.props.topic); }), // onConfirm
|
_ => this.props.onDeleteTopic(this.props.topic), // onConfirm
|
||||||
null, // "OK"
|
null, // "OK"
|
||||||
true, // Show Reject button
|
true, // Show Reject button
|
||||||
null // "Cancel"
|
null // "Cancel"
|
||||||
@@ -104,7 +104,7 @@ class TopicSecurity extends React.PureComponent {
|
|||||||
this.props.onShowAlert(
|
this.props.onShowAlert(
|
||||||
formatMessage(this.props.deleter ? messages.delete_messages : messages.clear_messages), // title
|
formatMessage(this.props.deleter ? messages.delete_messages : messages.clear_messages), // title
|
||||||
formatMessage(this.props.deleter ? messages.delete_messages_warning : messages.clear_messages_warning), // content
|
formatMessage(this.props.deleter ? messages.delete_messages_warning : messages.clear_messages_warning), // content
|
||||||
(() => { this.props.onDeleteMessages(this.props.topic); }), // onConfirm
|
_ => this.props.onDeleteMessages(this.props.topic), // onConfirm
|
||||||
null, // "OK"
|
null, // "OK"
|
||||||
true, // Show Reject button
|
true, // Show Reject button
|
||||||
null // "Cancel"
|
null // "Cancel"
|
||||||
@@ -117,7 +117,7 @@ class TopicSecurity extends React.PureComponent {
|
|||||||
this.props.onShowAlert(
|
this.props.onShowAlert(
|
||||||
formatMessage(messages.leave_chat), // title
|
formatMessage(messages.leave_chat), // title
|
||||||
formatMessage(messages.leave_chat_warning), // content
|
formatMessage(messages.leave_chat_warning), // content
|
||||||
(() => { this.props.onLeaveTopic(this.props.topic); }), // onConfirm
|
_ => this.props.onLeaveTopic(this.props.topic), // onConfirm
|
||||||
null, // "OK"
|
null, // "OK"
|
||||||
true, // Show Reject button
|
true, // Show Reject button
|
||||||
null // "Cancel"
|
null // "Cancel"
|
||||||
@@ -130,7 +130,7 @@ class TopicSecurity extends React.PureComponent {
|
|||||||
this.props.onShowAlert(
|
this.props.onShowAlert(
|
||||||
formatMessage(messages.block_contact), // title
|
formatMessage(messages.block_contact), // title
|
||||||
formatMessage(messages.block_contact_warning), // content
|
formatMessage(messages.block_contact_warning), // content
|
||||||
(() => { this.props.onBlockTopic(this.props.topic); }), // onConfirm
|
_ => this.props.onBlockTopic(this.props.topic), // onConfirm
|
||||||
null, // "OK"
|
null, // "OK"
|
||||||
true, // Show Reject button
|
true, // Show Reject button
|
||||||
null // "Cancel"
|
null // "Cancel"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default class VisiblePassword extends React.PureComponent {
|
|||||||
handleEditingFinished(e) {
|
handleEditingFinished(e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
let currentTarget = e.currentTarget;
|
let currentTarget = e.currentTarget;
|
||||||
setTimeout(() => {
|
setTimeout(_ => {
|
||||||
if (!currentTarget.contains(document.activeElement)) {
|
if (!currentTarget.contains(document.activeElement)) {
|
||||||
if (this.props.onFinished) {
|
if (this.props.onFinished) {
|
||||||
this.props.onFinished(this.state.value);
|
this.props.onFinished(this.state.value);
|
||||||
|
|||||||
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
+382
-1535
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
@@ -0,0 +1,284 @@
|
|||||||
|
/******/ (() => { // webpackBootstrap
|
||||||
|
/******/ "use strict";
|
||||||
|
/******/ var __webpack_modules__ = ({});
|
||||||
|
/************************************************************************/
|
||||||
|
/******/ // The module cache
|
||||||
|
/******/ var __webpack_module_cache__ = {};
|
||||||
|
/******/
|
||||||
|
/******/ // The require function
|
||||||
|
/******/ function __webpack_require__(moduleId) {
|
||||||
|
/******/ // Check if module is in cache
|
||||||
|
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
||||||
|
/******/ if (cachedModule !== undefined) {
|
||||||
|
/******/ return cachedModule.exports;
|
||||||
|
/******/ }
|
||||||
|
/******/ // Create a new module (and put it into the cache)
|
||||||
|
/******/ var module = __webpack_module_cache__[moduleId] = {
|
||||||
|
/******/ // no module.id needed
|
||||||
|
/******/ // no module.loaded needed
|
||||||
|
/******/ exports: {}
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Execute the module function
|
||||||
|
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||||
|
/******/
|
||||||
|
/******/ // Return the exports of the module
|
||||||
|
/******/ return module.exports;
|
||||||
|
/******/ }
|
||||||
|
/******/
|
||||||
|
/******/ // expose the modules object (__webpack_modules__)
|
||||||
|
/******/ __webpack_require__.m = __webpack_modules__;
|
||||||
|
/******/
|
||||||
|
/************************************************************************/
|
||||||
|
/******/ /* webpack/runtime/chunk loaded */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ var deferred = [];
|
||||||
|
/******/ __webpack_require__.O = (result, chunkIds, fn, priority) => {
|
||||||
|
/******/ if(chunkIds) {
|
||||||
|
/******/ priority = priority || 0;
|
||||||
|
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
|
||||||
|
/******/ deferred[i] = [chunkIds, fn, priority];
|
||||||
|
/******/ return;
|
||||||
|
/******/ }
|
||||||
|
/******/ var notFulfilled = Infinity;
|
||||||
|
/******/ for (var i = 0; i < deferred.length; i++) {
|
||||||
|
/******/ var [chunkIds, fn, priority] = deferred[i];
|
||||||
|
/******/ var fulfilled = true;
|
||||||
|
/******/ for (var j = 0; j < chunkIds.length; j++) {
|
||||||
|
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
|
||||||
|
/******/ chunkIds.splice(j--, 1);
|
||||||
|
/******/ } else {
|
||||||
|
/******/ fulfilled = false;
|
||||||
|
/******/ if(priority < notFulfilled) notFulfilled = priority;
|
||||||
|
/******/ }
|
||||||
|
/******/ }
|
||||||
|
/******/ if(fulfilled) {
|
||||||
|
/******/ deferred.splice(i--, 1)
|
||||||
|
/******/ var r = fn();
|
||||||
|
/******/ if (r !== undefined) result = r;
|
||||||
|
/******/ }
|
||||||
|
/******/ }
|
||||||
|
/******/ return result;
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/compat get default export */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||||
|
/******/ __webpack_require__.n = (module) => {
|
||||||
|
/******/ var getter = module && module.__esModule ?
|
||||||
|
/******/ () => (module['default']) :
|
||||||
|
/******/ () => (module);
|
||||||
|
/******/ __webpack_require__.d(getter, { a: getter });
|
||||||
|
/******/ return getter;
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/define property getters */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // define getter functions for harmony exports
|
||||||
|
/******/ __webpack_require__.d = (exports, definition) => {
|
||||||
|
/******/ for(var key in definition) {
|
||||||
|
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
||||||
|
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
||||||
|
/******/ }
|
||||||
|
/******/ }
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/ensure chunk */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.f = {};
|
||||||
|
/******/ // This file contains only the entry chunk.
|
||||||
|
/******/ // The chunk loading function for additional chunks
|
||||||
|
/******/ __webpack_require__.e = (chunkId) => {
|
||||||
|
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
|
||||||
|
/******/ __webpack_require__.f[key](chunkId, promises);
|
||||||
|
/******/ return promises;
|
||||||
|
/******/ }, []));
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/get javascript chunk filename */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // This function allow to reference async chunks
|
||||||
|
/******/ __webpack_require__.u = (chunkId) => {
|
||||||
|
/******/ // return url for filenames based on template
|
||||||
|
/******/ return "" + chunkId + ".dev.js";
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/global */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.g = (function() {
|
||||||
|
/******/ if (typeof globalThis === 'object') return globalThis;
|
||||||
|
/******/ try {
|
||||||
|
/******/ return this || new Function('return this')();
|
||||||
|
/******/ } catch (e) {
|
||||||
|
/******/ if (typeof window === 'object') return window;
|
||||||
|
/******/ }
|
||||||
|
/******/ })();
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/load script */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ var inProgress = {};
|
||||||
|
/******/ var dataWebpackPrefix = "tinode-webapp:";
|
||||||
|
/******/ // loadScript function to load a script via script tag
|
||||||
|
/******/ __webpack_require__.l = (url, done, key, chunkId) => {
|
||||||
|
/******/ if(inProgress[url]) { inProgress[url].push(done); return; }
|
||||||
|
/******/ var script, needAttach;
|
||||||
|
/******/ if(key !== undefined) {
|
||||||
|
/******/ var scripts = document.getElementsByTagName("script");
|
||||||
|
/******/ for(var i = 0; i < scripts.length; i++) {
|
||||||
|
/******/ var s = scripts[i];
|
||||||
|
/******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
|
||||||
|
/******/ }
|
||||||
|
/******/ }
|
||||||
|
/******/ if(!script) {
|
||||||
|
/******/ needAttach = true;
|
||||||
|
/******/ script = document.createElement('script');
|
||||||
|
/******/
|
||||||
|
/******/ script.charset = 'utf-8';
|
||||||
|
/******/ script.timeout = 120;
|
||||||
|
/******/ if (__webpack_require__.nc) {
|
||||||
|
/******/ script.setAttribute("nonce", __webpack_require__.nc);
|
||||||
|
/******/ }
|
||||||
|
/******/ script.setAttribute("data-webpack", dataWebpackPrefix + key);
|
||||||
|
/******/ script.src = url;
|
||||||
|
/******/ }
|
||||||
|
/******/ inProgress[url] = [done];
|
||||||
|
/******/ var onScriptComplete = (prev, event) => {
|
||||||
|
/******/ // avoid mem leaks in IE.
|
||||||
|
/******/ script.onerror = script.onload = null;
|
||||||
|
/******/ clearTimeout(timeout);
|
||||||
|
/******/ var doneFns = inProgress[url];
|
||||||
|
/******/ delete inProgress[url];
|
||||||
|
/******/ script.parentNode && script.parentNode.removeChild(script);
|
||||||
|
/******/ doneFns && doneFns.forEach((fn) => (fn(event)));
|
||||||
|
/******/ if(prev) return prev(event);
|
||||||
|
/******/ };
|
||||||
|
/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
|
||||||
|
/******/ script.onerror = onScriptComplete.bind(null, script.onerror);
|
||||||
|
/******/ script.onload = onScriptComplete.bind(null, script.onload);
|
||||||
|
/******/ needAttach && document.head.appendChild(script);
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/make namespace object */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // define __esModule on exports
|
||||||
|
/******/ __webpack_require__.r = (exports) => {
|
||||||
|
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||||
|
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||||
|
/******/ }
|
||||||
|
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/publicPath */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.p = "/umd/";
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/jsonp chunk loading */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // no baseURI
|
||||||
|
/******/
|
||||||
|
/******/ // object to store loaded and loading chunks
|
||||||
|
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
|
||||||
|
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
|
||||||
|
/******/ var installedChunks = {
|
||||||
|
/******/ "runtime": 0
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ __webpack_require__.f.j = (chunkId, promises) => {
|
||||||
|
/******/ // JSONP chunk loading for javascript
|
||||||
|
/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
|
||||||
|
/******/ if(installedChunkData !== 0) { // 0 means "already installed".
|
||||||
|
/******/
|
||||||
|
/******/ // a Promise means "currently loading".
|
||||||
|
/******/ if(installedChunkData) {
|
||||||
|
/******/ promises.push(installedChunkData[2]);
|
||||||
|
/******/ } else {
|
||||||
|
/******/ if("runtime" != chunkId) {
|
||||||
|
/******/ // setup Promise in chunk cache
|
||||||
|
/******/ var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
|
||||||
|
/******/ promises.push(installedChunkData[2] = promise);
|
||||||
|
/******/
|
||||||
|
/******/ // start chunk loading
|
||||||
|
/******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId);
|
||||||
|
/******/ // create error before stack unwound to get useful stacktrace later
|
||||||
|
/******/ var error = new Error();
|
||||||
|
/******/ var loadingEnded = (event) => {
|
||||||
|
/******/ if(__webpack_require__.o(installedChunks, chunkId)) {
|
||||||
|
/******/ installedChunkData = installedChunks[chunkId];
|
||||||
|
/******/ if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
|
||||||
|
/******/ if(installedChunkData) {
|
||||||
|
/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type);
|
||||||
|
/******/ var realSrc = event && event.target && event.target.src;
|
||||||
|
/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
|
||||||
|
/******/ error.name = 'ChunkLoadError';
|
||||||
|
/******/ error.type = errorType;
|
||||||
|
/******/ error.request = realSrc;
|
||||||
|
/******/ installedChunkData[1](error);
|
||||||
|
/******/ }
|
||||||
|
/******/ }
|
||||||
|
/******/ };
|
||||||
|
/******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
|
||||||
|
/******/ } else installedChunks[chunkId] = 0;
|
||||||
|
/******/ }
|
||||||
|
/******/ }
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // no prefetching
|
||||||
|
/******/
|
||||||
|
/******/ // no preloaded
|
||||||
|
/******/
|
||||||
|
/******/ // no HMR
|
||||||
|
/******/
|
||||||
|
/******/ // no HMR manifest
|
||||||
|
/******/
|
||||||
|
/******/ __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
|
||||||
|
/******/
|
||||||
|
/******/ // install a JSONP callback for chunk loading
|
||||||
|
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
|
||||||
|
/******/ var [chunkIds, moreModules, runtime] = data;
|
||||||
|
/******/ // add "moreModules" to the modules object,
|
||||||
|
/******/ // then flag all "chunkIds" as loaded and fire callback
|
||||||
|
/******/ var moduleId, chunkId, i = 0;
|
||||||
|
/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
|
||||||
|
/******/ for(moduleId in moreModules) {
|
||||||
|
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
|
||||||
|
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
|
||||||
|
/******/ }
|
||||||
|
/******/ }
|
||||||
|
/******/ if(runtime) var result = runtime(__webpack_require__);
|
||||||
|
/******/ }
|
||||||
|
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
|
||||||
|
/******/ for(;i < chunkIds.length; i++) {
|
||||||
|
/******/ chunkId = chunkIds[i];
|
||||||
|
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
|
||||||
|
/******/ installedChunks[chunkId][0]();
|
||||||
|
/******/ }
|
||||||
|
/******/ installedChunks[chunkId] = 0;
|
||||||
|
/******/ }
|
||||||
|
/******/ return __webpack_require__.O(result);
|
||||||
|
/******/ }
|
||||||
|
/******/
|
||||||
|
/******/ var chunkLoadingGlobal = globalThis["webpackChunktinode_webapp"] = globalThis["webpackChunktinode_webapp"] || [];
|
||||||
|
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
|
||||||
|
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/************************************************************************/
|
||||||
|
/******/
|
||||||
|
/******/
|
||||||
|
/******/ })()
|
||||||
|
;
|
||||||
|
//# sourceMappingURL=runtime.dev.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
|||||||
|
(()=>{"use strict";var e,r,t,o={},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var t=n[e]={exports:{}};return o[e].call(t.exports,t,t.exports,a),t.exports}a.m=o,e=[],a.O=(r,t,o,n)=>{if(!t){var i=1/0;for(s=0;s<e.length;s++){for(var[t,o,n]=e[s],l=!0,u=0;u<t.length;u++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](t[u])))?t.splice(u--,1):(l=!1,n<i&&(i=n));if(l){e.splice(s--,1);var d=o();void 0!==d&&(r=d)}}return r}n=n||0;for(var s=e.length;s>0&&e[s-1][2]>n;s--)e[s]=e[s-1];e[s]=[t,o,n]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((r,t)=>(a.f[t](e,r),r)),[])),a.u=e=>e+".prod.js",a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="tinode-webapp:",a.l=(e,o,n,i)=>{if(r[e])r[e].push(o);else{var l,u;if(void 0!==n)for(var d=document.getElementsByTagName("script"),s=0;s<d.length;s++){var p=d[s];if(p.getAttribute("src")==e||p.getAttribute("data-webpack")==t+n){l=p;break}}l||(u=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+n),l.src=e),r[e]=[o];var c=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach((e=>e(o))),t)return t(o)},f=setTimeout(c.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=c.bind(null,l.onerror),l.onload=c.bind(null,l.onload),u&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.p="/umd/",(()=>{var e={666:0};a.f.j=(r,t)=>{var o=a.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else if(666!=r){var n=new Promise(((t,n)=>o=e[r]=[t,n]));t.push(o[2]=n);var i=a.p+a.u(r),l=new Error;a.l(i,(t=>{if(a.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+r,r)}else e[r]=0},a.O.j=r=>0===e[r];var r=(r,t)=>{var o,n,[i,l,u]=t,d=0;if(i.some((r=>0!==e[r]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(u)var s=u(a)}for(r&&r(t);d<i.length;d++)n=i[d],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(s)},t=globalThis.webpackChunktinode_webapp=globalThis.webpackChunktinode_webapp||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})()})();
|
||||||
|
//# sourceMappingURL=runtime.prod.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,320 @@
|
|||||||
|
"use strict";
|
||||||
|
(globalThis["webpackChunktinode_webapp"] = globalThis["webpackChunktinode_webapp"] || []).push([["src_widgets_audio-recorder_jsx"],{
|
||||||
|
|
||||||
|
/***/ "./src/widgets/audio-recorder.jsx":
|
||||||
|
/*!****************************************!*\
|
||||||
|
!*** ./src/widgets/audio-recorder.jsx ***!
|
||||||
|
\****************************************/
|
||||||
|
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
|
__webpack_require__.r(__webpack_exports__);
|
||||||
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||||
|
/* harmony export */ "default": () => (/* binding */ AudioRecorder)
|
||||||
|
/* 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");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const BUFFER_SIZE = 256;
|
||||||
|
const CANVAS_UPSCALING = 2.0;
|
||||||
|
const LINE_WIDTH = 3 * CANVAS_UPSCALING;
|
||||||
|
const SPACING = 2 * CANVAS_UPSCALING;
|
||||||
|
const MILLIS_PER_BAR = 100;
|
||||||
|
const BAR_COLOR = '#BBBD';
|
||||||
|
const BAR_SCALE = 64.0;
|
||||||
|
const VISUALIZATION_BARS = 96;
|
||||||
|
const MAX_SAMPLES_PER_BAR = 10;
|
||||||
|
const AUDIO_MIME_TYPE = 'audio/webm';
|
||||||
|
class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
enabled: true,
|
||||||
|
audioRecord: null,
|
||||||
|
recording: true,
|
||||||
|
paused: false,
|
||||||
|
duration: '0:00',
|
||||||
|
blobUrl: null,
|
||||||
|
preview: null
|
||||||
|
};
|
||||||
|
this.visualize = this.visualize.bind(this);
|
||||||
|
this.initMediaRecording = this.initMediaRecording.bind(this);
|
||||||
|
this.initCanvas = this.initCanvas.bind(this);
|
||||||
|
this.getRecording = this.getRecording.bind(this);
|
||||||
|
this.cleanUp = this.cleanUp.bind(this);
|
||||||
|
this.handleResume = this.handleResume.bind(this);
|
||||||
|
this.handlePause = this.handlePause.bind(this);
|
||||||
|
this.handleDelete = this.handleDelete.bind(this);
|
||||||
|
this.handleDone = this.handleDone.bind(this);
|
||||||
|
this.durationMillis = 0;
|
||||||
|
this.startedOn = null;
|
||||||
|
this.viewBuffer = [];
|
||||||
|
this.canvasRef = react__WEBPACK_IMPORTED_MODULE_0___default().createRef();
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
this.stream = null;
|
||||||
|
this.mediaRecorder = null;
|
||||||
|
this.audioContext = null;
|
||||||
|
this.audioInput = null;
|
||||||
|
this.analyser = null;
|
||||||
|
this.audioChunks = [];
|
||||||
|
try {
|
||||||
|
navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: true,
|
||||||
|
video: false
|
||||||
|
}).then(this.initMediaRecording, this.props.onError);
|
||||||
|
} catch (err) {
|
||||||
|
this.props.onError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.startedOn = null;
|
||||||
|
if (this.stream) {
|
||||||
|
this.cleanUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visualize() {
|
||||||
|
this.initCanvas();
|
||||||
|
const pcmData = new Uint8Array(this.analyser.frequencyBinCount);
|
||||||
|
const width = this.canvasWidth;
|
||||||
|
const height = this.canvasHeight;
|
||||||
|
const viewLength = width / (LINE_WIDTH + SPACING) | 0;
|
||||||
|
const viewDuration = MILLIS_PER_BAR * viewLength;
|
||||||
|
this.canvasContext.lineWidth = LINE_WIDTH;
|
||||||
|
this.canvasContext.strokeStyle = BAR_COLOR;
|
||||||
|
let prevBarCount = 0;
|
||||||
|
let volume = 0.0;
|
||||||
|
let countPerBar = 0;
|
||||||
|
const drawFrame = _ => {
|
||||||
|
if (!this.startedOn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(drawFrame);
|
||||||
|
const duration = this.durationMillis + (Date.now() - this.startedOn);
|
||||||
|
this.setState({
|
||||||
|
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_4__.secondsToTime)(duration / 1000)
|
||||||
|
});
|
||||||
|
if (duration > _config_js__WEBPACK_IMPORTED_MODULE_5__.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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.analyser.getByteTimeDomainData(pcmData);
|
||||||
|
let amp = 0.0;
|
||||||
|
for (const amplitude of pcmData) {
|
||||||
|
amp += (amplitude - 127) ** 2;
|
||||||
|
}
|
||||||
|
volume += Math.sqrt(amp / pcmData.length);
|
||||||
|
countPerBar++;
|
||||||
|
let barCount = duration / MILLIS_PER_BAR | 0;
|
||||||
|
const dx = viewDuration > duration ? 0 : (duration - MILLIS_PER_BAR * barCount) / MILLIS_PER_BAR * (LINE_WIDTH + SPACING);
|
||||||
|
if (prevBarCount != barCount) {
|
||||||
|
prevBarCount = barCount;
|
||||||
|
this.viewBuffer.push(volume / countPerBar);
|
||||||
|
volume = 0.0;
|
||||||
|
countPerBar = 0;
|
||||||
|
if (this.viewBuffer.length > viewLength) {
|
||||||
|
this.viewBuffer.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.canvasContext.clearRect(0, 0, width, height);
|
||||||
|
this.canvasContext.beginPath();
|
||||||
|
for (let i = 0; i < this.viewBuffer.length; i++) {
|
||||||
|
let x = i * (LINE_WIDTH + SPACING) - dx;
|
||||||
|
let y = Math.min(this.viewBuffer[i] / BAR_SCALE, 0.9) * height;
|
||||||
|
this.canvasContext.moveTo(x, (height - y) * 0.5);
|
||||||
|
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
|
||||||
|
}
|
||||||
|
this.canvasContext.stroke();
|
||||||
|
};
|
||||||
|
drawFrame();
|
||||||
|
}
|
||||||
|
handlePause(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.mediaRecorder.pause();
|
||||||
|
this.mediaRecorder.requestData();
|
||||||
|
this.durationMillis += Date.now() - this.startedOn;
|
||||||
|
this.startedOn = null;
|
||||||
|
this.setState({
|
||||||
|
recording: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleResume(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.state.enabled) {
|
||||||
|
this.startedOn = Date.now();
|
||||||
|
this.mediaRecorder.resume();
|
||||||
|
this.setState({
|
||||||
|
recording: true
|
||||||
|
}, this.visualize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleDelete(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.durationMillis = 0;
|
||||||
|
this.startedOn = null;
|
||||||
|
this.mediaRecorder.stop();
|
||||||
|
this.cleanUp();
|
||||||
|
this.setState({
|
||||||
|
recording: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleDone(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({
|
||||||
|
recording: false
|
||||||
|
});
|
||||||
|
if (this.startedOn) {
|
||||||
|
this.durationMillis += Date.now() - this.startedOn;
|
||||||
|
this.startedOn = null;
|
||||||
|
}
|
||||||
|
if (this.mediaRecorder) {
|
||||||
|
this.mediaRecorder.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initCanvas() {
|
||||||
|
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
|
||||||
|
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
|
||||||
|
this.canvasContext = this.canvasRef.current.getContext('2d');
|
||||||
|
this.canvasContext.lineCap = 'round';
|
||||||
|
this.canvasContext.translate(0.5, 0.5);
|
||||||
|
this.canvasWidth = this.canvasRef.current.width;
|
||||||
|
this.canvasHeight = this.canvasRef.current.height;
|
||||||
|
}
|
||||||
|
initMediaRecording(stream) {
|
||||||
|
this.stream = stream;
|
||||||
|
this.mediaRecorder = new MediaRecorder(stream, {
|
||||||
|
mimeType: AUDIO_MIME_TYPE,
|
||||||
|
audioBitsPerSecond: 24_000
|
||||||
|
});
|
||||||
|
this.audioContext = new AudioContext();
|
||||||
|
this.audioInput = this.audioContext.createMediaStreamSource(stream);
|
||||||
|
this.analyser = this.audioContext.createAnalyser();
|
||||||
|
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));
|
||||||
|
} else {
|
||||||
|
this.props.onDeleted();
|
||||||
|
}
|
||||||
|
this.cleanUp();
|
||||||
|
};
|
||||||
|
this.mediaRecorder.ondataavailable = e => {
|
||||||
|
if (e.data.size > 0) {
|
||||||
|
this.audioChunks.push(e.data);
|
||||||
|
}
|
||||||
|
if (this.mediaRecorder.state != 'inactive') {
|
||||||
|
this.getRecording(this.mediaRecorder.mimeType).then(result => {
|
||||||
|
this.setState({
|
||||||
|
blobUrl: result.url,
|
||||||
|
preview: result.preview
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.durationMillis = 0;
|
||||||
|
this.startedOn = Date.now();
|
||||||
|
this.mediaRecorder.start();
|
||||||
|
this.visualize();
|
||||||
|
}
|
||||||
|
getRecording(mimeType, duration) {
|
||||||
|
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 => {
|
||||||
|
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)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
createPreview(audio) {
|
||||||
|
const data = audio.getChannelData(0);
|
||||||
|
const viewLength = Math.min(data.length, VISUALIZATION_BARS);
|
||||||
|
const totalSPB = data.length / viewLength | 0;
|
||||||
|
const samplingRate = Math.max(1, totalSPB / MAX_SAMPLES_PER_BAR | 0);
|
||||||
|
let buffer = [];
|
||||||
|
let max = -1;
|
||||||
|
for (let i = 0; i < viewLength; i++) {
|
||||||
|
let amplitude = 0;
|
||||||
|
let count = 0;
|
||||||
|
for (let j = 0; j < totalSPB; j += samplingRate) {
|
||||||
|
amplitude += data[totalSPB * i + j] ** 2;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
const val = Math.sqrt(amplitude / count);
|
||||||
|
buffer.push(val);
|
||||||
|
max = Math.max(max, val);
|
||||||
|
}
|
||||||
|
if (max > 0) {
|
||||||
|
buffer = buffer.map(a => 100 * a / max | 0);
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
cleanUp() {
|
||||||
|
this.audioInput.disconnect();
|
||||||
|
this.stream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
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"
|
||||||
|
}, 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"], {
|
||||||
|
src: this.state.blobUrl,
|
||||||
|
preview: this.state.preview,
|
||||||
|
duration: this.durationMillis,
|
||||||
|
short: true
|
||||||
|
}), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||||
|
className: "duration"
|
||||||
|
}, this.state.duration), this.state.recording ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||||
|
href: "#",
|
||||||
|
onClick: this.handlePause,
|
||||||
|
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"
|
||||||
|
}, 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"
|
||||||
|
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||||
|
className: "material-icons"
|
||||||
|
}, "send")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***/ })
|
||||||
|
|
||||||
|
}]);
|
||||||
|
//# sourceMappingURL=src_widgets_audio-recorder_jsx.dev.js.map
|
||||||
File diff suppressed because one or more lines are too long
+48
-41
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 it is too large
Load Diff
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user