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
|
||||
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
|
||||
umd/* binary
|
||||
package-lock.json binary
|
||||
|
||||
+2
-1
@@ -949,6 +949,7 @@ i.search {
|
||||
/* partial bottom border */
|
||||
background: linear-gradient(to right, transparent 3.5rem, #eee 1px, #eee 100%) 0 100% / 100% 1px no-repeat, #fff;
|
||||
overflow-x: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact-box.small > li {
|
||||
@@ -1944,7 +1945,7 @@ video {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 36rem;
|
||||
overflow-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
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": {
|
||||
"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:dev": "webpack --mode development",
|
||||
"build:prod": "webpack --mode production",
|
||||
@@ -21,12 +22,8 @@
|
||||
},
|
||||
"browserslist": "> 0.5%, not IE 11, not op_mini all, not and_uc >0",
|
||||
"files": [
|
||||
"umd/index.prod.js",
|
||||
"umd/index.prod.js.map",
|
||||
"umd/index.dev.js",
|
||||
"umd/index.dev.js.map",
|
||||
"umd/tinode.prod.js",
|
||||
"umd/tinode.dev.js",
|
||||
"umd/*.js",
|
||||
"umd/*.map",
|
||||
"audio/*.m4a",
|
||||
"css/base.css",
|
||||
"css/base.min.css",
|
||||
@@ -68,12 +65,11 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^9.14.0",
|
||||
"fix-webm-duration": "^1.0.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-intl": "^6.2.1",
|
||||
"tinode-sdk": "^0.21.0-beta1",
|
||||
"webrtc-adapter": "^8.2.0"
|
||||
"tinode-sdk": "^0.20.2",
|
||||
"webm-duration-fix": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 BASE_LANG = 'en';
|
||||
const LANG_DIR = './src/i18n/';
|
||||
const LANG_PATTERN = './src/i18n/*.json';
|
||||
const LANG_PATTERN_ONE = './src/i18n/%s.json';
|
||||
const FINAL_FILE = './src/messages.json';
|
||||
const OUTPUT_PATTERN = './src/i18n.min/%s.json';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
@@ -99,8 +99,9 @@ globSync(LANG_PATTERN)
|
||||
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.
|
||||
// New way: Emit one file per language with all unnecessary data stripped.
|
||||
|
||||
// Read all translations.
|
||||
const messages = globSync(LANG_PATTERN)
|
||||
@@ -122,6 +123,11 @@ const messages = globSync(LANG_PATTERN)
|
||||
return { ...collection, ...descriptors };
|
||||
}, {});
|
||||
|
||||
// Write a file with all translations combined.
|
||||
// Legacy: Write a single file with all translations combined.
|
||||
mkdirpSync(path.dirname(FINAL_FILE));
|
||||
fs.writeFileSync(FINAL_FILE, JSON.stringify(messages));
|
||||
|
||||
// New way: write minified ptraslation files.
|
||||
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.
|
||||
export const FORWARDED_PREVIEW_LENGTH = 84;
|
||||
// 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).
|
||||
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 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:
|
||||
// Check parameters from URL hash #?hl=ru, then browser, then use 'en' as a fallback.
|
||||
const { params } = HashNavigation.parseUrlHash(window.location.hash);
|
||||
@@ -19,11 +34,15 @@ const language = (params && params.hl) ||
|
||||
navigator.userLanguage ||
|
||||
'en';
|
||||
|
||||
// Get the base language 'en' from a more specific 'en_GB' or 'en-US' as a partial fallback.
|
||||
const baseLanguage = language.toLowerCase().split(/[-_]/)[0];
|
||||
// Make sure names like 'en_GB' and 'en-GB' consistently use '-'.
|
||||
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'.
|
||||
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];
|
||||
|
||||
// Set lang attribute of the HTML element: <html lang="XX">
|
||||
@@ -32,9 +51,7 @@ document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
|
||||
// Render the app.
|
||||
const root = createRoot(document.getElementById('mountPoint'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
|
||||
<TinodeWeb />
|
||||
</IntlProvider>
|
||||
</React.StrictMode>
|
||||
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
|
||||
<TinodeWeb />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
@@ -138,10 +138,10 @@ export function imageCrop(mime, objURL, left, top, width, height, scale) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.onerror = (err) => {
|
||||
img.onerror = _ => {
|
||||
reject(new Error("Image format unrecognized"));
|
||||
};
|
||||
img.onload = () => {
|
||||
img.onload = _ => {
|
||||
// Once the image is loaded, the URL is no longer needed.
|
||||
URL.revokeObjectURL(img.src);
|
||||
|
||||
@@ -175,7 +175,7 @@ export function fileToBase64(file) {
|
||||
reader.onerror = (evt) => {
|
||||
reject(reader.error);
|
||||
};
|
||||
reader.onload = () => {
|
||||
reader.onload = _ => {
|
||||
resolve({mime: file.type, bits: reader.result.split(',')[1], name: file.name});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
@@ -189,7 +189,7 @@ export function blobToBase64(blob) {
|
||||
reader.onerror = _ => {
|
||||
reject(reader.error);
|
||||
};
|
||||
reader.onload = () => {
|
||||
reader.onload = _ => {
|
||||
resolve({mime: blob.type, bits: reader.result.split(',')[1]});
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
|
||||
@@ -412,8 +412,10 @@ export function replyFormatter(style, data, values, key, stack) {
|
||||
try {
|
||||
loadedPromise = cancelablePromise(quoteImage.call(this, data));
|
||||
} catch (error) {
|
||||
console.error("Failed to quote image", error);
|
||||
loadedPromise = cancelablePromise(error);
|
||||
}
|
||||
|
||||
attr.whenDone = loadedPromise;
|
||||
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
|
||||
return React.createElement(React.Fragment, {key: key}, values);
|
||||
|
||||
@@ -76,7 +76,7 @@ class AccSecurityView extends React.Component {
|
||||
this.props.onShowAlert(
|
||||
formatMessage(messages.delete_account), // title
|
||||
formatMessage(messages.delete_account_warning), // content
|
||||
(() => { this.props.onDeleteAccount(); }), // onConfirm
|
||||
(_ => this.props.onDeleteAccount()), // onConfirm
|
||||
null, // use default text "OK"
|
||||
true, // Show Reject button
|
||||
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
|
||||
// for uploading it to the server out of band.
|
||||
uploadAvatar(mime, blob, width, height) {
|
||||
const readyToUpload = (image) => {
|
||||
const readyToUpload = image => {
|
||||
let {mime, blob} = image;
|
||||
this.setState({imageUrl: URL.createObjectURL(blob), buttonDisabled: true});
|
||||
if (blob.size > MAX_AVATAR_BYTES) {
|
||||
|
||||
@@ -410,11 +410,10 @@ class InfoView extends React.Component {
|
||||
}
|
||||
|
||||
const isMe = this.props.tinode.isMe(params.topicName);
|
||||
const menuItems = [
|
||||
{title: formatMessage(messages.edit_permissions), handler: () => {
|
||||
this.handleLaunchPermissionsEditor(isMe ? 'want' : 'user', params.topicName);
|
||||
}}
|
||||
];
|
||||
const menuItems = [{
|
||||
title: formatMessage(messages.edit_permissions),
|
||||
handler: _ => this.handleLaunchPermissionsEditor(isMe ? 'want' : 'user', params.topicName)
|
||||
}];
|
||||
if (!isMe) {
|
||||
menuItems.push('member_delete');
|
||||
}
|
||||
|
||||
+17
-15
@@ -400,7 +400,7 @@ class MessagesView extends React.Component {
|
||||
|
||||
const setQuery = newTopic ? this.props.newTopicParams : undefined;
|
||||
topic.subscribe(getQuery.build(), setQuery)
|
||||
.then((ctrl) => {
|
||||
.then(ctrl => {
|
||||
if (ctrl.code == 303) {
|
||||
// Redirect to another topic requested.
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', ctrl.params.topic));
|
||||
@@ -430,7 +430,7 @@ class MessagesView extends React.Component {
|
||||
topic.delMessagesList(calls, true);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
console.error("Failed subscription to", this.state.topic, err);
|
||||
this.props.onError(err.message, 'err');
|
||||
const blankState = MessagesView.getDerivedStateFromProps({}, {});
|
||||
@@ -447,8 +447,8 @@ class MessagesView extends React.Component {
|
||||
const oldTopic = this.props.tinode.getTopic(oldTopicName);
|
||||
if (oldTopic && oldTopic.isSubscribed()) {
|
||||
oldTopic.leave(false)
|
||||
.catch(() => { /* do nothing here */ })
|
||||
.finally(() => {
|
||||
.catch(_ => { /* do nothing here */ })
|
||||
.finally(_ => {
|
||||
// We don't care if the request succeeded or failed.
|
||||
// The topic is dead regardless.
|
||||
this.setState({fetchingMessages: false});
|
||||
@@ -487,10 +487,10 @@ class MessagesView extends React.Component {
|
||||
if (event.target.scrollTop <= 0) {
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) {
|
||||
this.setState({fetchingMessages: true}, () => {
|
||||
this.setState({fetchingMessages: true}, _ => {
|
||||
topic.getMessagesPage(MESSAGES_PAGE)
|
||||
.catch((err) => this.props.onError(err.message, 'err'))
|
||||
.finally(() => this.setState({fetchingMessages: false}));
|
||||
.catch(err => this.props.onError(err.message, 'err'))
|
||||
.finally(_ => this.setState({fetchingMessages: false}));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -656,9 +656,7 @@ class MessagesView extends React.Component {
|
||||
switch (info.what) {
|
||||
case 'kp': {
|
||||
clearTimeout(this.keyPressTimer);
|
||||
this.keyPressTimer = setTimeout(() => {
|
||||
this.setState({typingIndicator: false});
|
||||
}, KEYPRESS_DELAY + 1000);
|
||||
this.keyPressTimer = setTimeout(_ => this.setState({typingIndicator: false}), KEYPRESS_DELAY + 1000);
|
||||
if (!this.state.typingIndicator) {
|
||||
this.setState({typingIndicator: true});
|
||||
}
|
||||
@@ -762,6 +760,11 @@ class MessagesView extends React.Component {
|
||||
this.handleCancelReply();
|
||||
} else if (this.state.reply) {
|
||||
if (this.state.reply.editing) {
|
||||
if (msg == this.state.contentToEdit) {
|
||||
// Message unchanged.
|
||||
this.handleCancelReply();
|
||||
return;
|
||||
}
|
||||
// Editing an existing message.
|
||||
head = {replace: ':' + this.state.reply.seq};
|
||||
} else if (this.state.reply.content) {
|
||||
@@ -888,9 +891,8 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
// Pass data and the uploader to the TinodeWeb.
|
||||
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||
}).catch((err) => {
|
||||
this.props.onError(err, 'err');
|
||||
});
|
||||
})
|
||||
.catch(err => this.props.onError(err, 'err'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1032,7 +1034,7 @@ class MessagesView extends React.Component {
|
||||
|
||||
this.setState({
|
||||
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,
|
||||
editing: true
|
||||
},
|
||||
@@ -1199,7 +1201,7 @@ class MessagesView extends React.Component {
|
||||
content={msg.content}
|
||||
mimeType={msg.head && msg.head.mime}
|
||||
replyToSeq={replyToSeq}
|
||||
edited={msg.head && msg.head.replace && !msg.head.webrtc}
|
||||
edited={msg.head && !msg.head.webrtc && msg.head.replace}
|
||||
timestamp={msg.ts}
|
||||
response={isReply}
|
||||
seq={msg.seq}
|
||||
|
||||
@@ -109,7 +109,8 @@ class NewTopicView extends React.Component {
|
||||
{this.state.tabSelected === 'grp' ?
|
||||
<NewTopicGroup
|
||||
tinode={this.props.tinode}
|
||||
onSubmit={this.handleNewGroupSubmit} /> :
|
||||
onSubmit={this.handleNewGroupSubmit}
|
||||
onError={this.props.onError} /> :
|
||||
this.state.tabSelected === 'byid' ?
|
||||
<NewTopicById
|
||||
onSubmit={this.handleGroupByID}
|
||||
|
||||
@@ -35,9 +35,7 @@ export default class PasswordResetView extends React.PureComponent {
|
||||
} else {
|
||||
const email = this.state.email.trim();
|
||||
this.setState({email: email});
|
||||
this.props.onRequest('email', email).then(() => {
|
||||
this.setState({sent: true});
|
||||
});
|
||||
this.props.onRequest('email', email).then(_ => this.setState({sent: true}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+74
-105
@@ -303,7 +303,7 @@ class TinodeWeb extends React.Component {
|
||||
if (this.state.desktopAlerts) {
|
||||
this.tinode.setDeviceToken(this.state.firebaseToken);
|
||||
}
|
||||
}).catch(() => {
|
||||
}).catch(_ => {
|
||||
// do nothing: handled earlier.
|
||||
// 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.
|
||||
token.expires = new Date(token.expires);
|
||||
this.tinode.setAuthToken(token);
|
||||
this.tinode.connect().catch((err) => {
|
||||
this.tinode.connect().catch(err => {
|
||||
// Socket error
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
@@ -400,7 +400,7 @@ class TinodeWeb extends React.Component {
|
||||
// (a) for channels always,
|
||||
// (b) pushes when the app is in foreground but has no focus.
|
||||
firebaseOnMessage(this.fcm, (payload) => { this.handlePushMessage(payload); });
|
||||
}).catch((err) => {
|
||||
}).catch(err => {
|
||||
// SW registration or FCM has failed :(
|
||||
onError(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.
|
||||
checkForAppUpdate(reg) {
|
||||
reg.onupdatefound = () => {
|
||||
reg.onupdatefound = _ => {
|
||||
const installingWorker = reg.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
installingWorker.onstatechange = _ => {
|
||||
if (installingWorker.state == 'installed' && navigator.serviceWorker.controller) {
|
||||
const msg = <>
|
||||
<FormattedMessage id="update_available"
|
||||
@@ -570,7 +570,7 @@ class TinodeWeb extends React.Component {
|
||||
if (this.tinode.isConnected()) {
|
||||
this.doLogin(login, password, null, {meth: this.state.credMethod, resp: this.state.credCode});
|
||||
} else {
|
||||
this.tinode.connect().catch((err) => {
|
||||
this.tinode.connect().catch(err => {
|
||||
// Socket error
|
||||
this.setState({loginDisabled: false, autoLogin: false, loadSpinnerVisible: false});
|
||||
this.handleError(err.message, 'err');
|
||||
@@ -581,12 +581,12 @@ class TinodeWeb extends React.Component {
|
||||
// Enable or disable saving the password and IndexedDB.
|
||||
handlePersistenceChange(persist) {
|
||||
if (persist) {
|
||||
this.tinode.initStorage().then(() => {
|
||||
this.tinode.initStorage().then(_ => {
|
||||
LocalStorageUtil.setObject('keep-logged-in', true);
|
||||
this.setState({persist: true});
|
||||
});
|
||||
} else {
|
||||
this.tinode.clearStorage().then(() => {
|
||||
this.tinode.clearStorage().then(_ => {
|
||||
LocalStorageUtil.setObject('keep-logged-in', false);
|
||||
this.setState({persist: false});
|
||||
});
|
||||
@@ -625,7 +625,7 @@ class TinodeWeb extends React.Component {
|
||||
prom.then(_ => {
|
||||
// Reconnected: clear error
|
||||
this.handleError();
|
||||
}).catch((err) => {
|
||||
}).catch(err => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
return;
|
||||
@@ -701,7 +701,7 @@ class TinodeWeb extends React.Component {
|
||||
loginPromise = connectionPromise.then(_ => this.tinode.loginToken(token, cred));
|
||||
}
|
||||
|
||||
loginPromise.then((ctrl) => {
|
||||
loginPromise.then(ctrl => {
|
||||
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
|
||||
this.setState({loadSpinnerVisible: false});
|
||||
if (cred) {
|
||||
@@ -711,7 +711,7 @@ class TinodeWeb extends React.Component {
|
||||
} else {
|
||||
this.handleLoginSuccessful();
|
||||
}
|
||||
}).catch((err) => {
|
||||
}).catch(err => {
|
||||
// Connection or login failed, report error.
|
||||
this.setState({
|
||||
loginDisabled: false,
|
||||
@@ -768,12 +768,12 @@ class TinodeWeb extends React.Component {
|
||||
withTags().
|
||||
withCred().
|
||||
build()
|
||||
).catch((err) => {
|
||||
).catch(err => {
|
||||
this.tinode.disconnect();
|
||||
localStorage.removeItem('auth-token');
|
||||
this.handleError(err.message, 'err');
|
||||
HashNavigation.navigateTo('');
|
||||
}).finally(() => {
|
||||
}).finally(_ => {
|
||||
this.setState({loadSpinnerVisible: false});
|
||||
});
|
||||
let urlHash = HashNavigation.setUrlSidePanel(window.location.hash, 'contacts');
|
||||
@@ -934,7 +934,7 @@ class TinodeWeb extends React.Component {
|
||||
if (fnd.isSubscribed()) {
|
||||
this.tnFndSubsUpdated();
|
||||
} else {
|
||||
fnd.subscribe(fnd.startMetaQuery().withSub().build()).catch((err) => {
|
||||
fnd.subscribe(fnd.startMetaQuery().withSub().build()).catch(err => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
@@ -957,11 +957,9 @@ class TinodeWeb extends React.Component {
|
||||
*/
|
||||
handleSearchContacts(query) {
|
||||
const fnd = this.tinode.getFndTopic();
|
||||
fnd.setMeta({desc: {public: query}}).then((ctrl) => {
|
||||
return fnd.getMeta(fnd.startMetaQuery().withSub().build());
|
||||
}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
fnd.setMeta({desc: {public: query}})
|
||||
.then(_ => fnd.getMeta(fnd.startMetaQuery().withSub().build()))
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
|
||||
// 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.
|
||||
const subscribePromise =
|
||||
topic.subscribe()
|
||||
.then(() => {
|
||||
.then(_ => {
|
||||
// If there are unsent messages (except video call messages),
|
||||
// try sending them now. Unsent video call messages will be dropped.
|
||||
let calls = [];
|
||||
@@ -1069,15 +1067,13 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
// TODO: check if return is required.
|
||||
return topic.publishDraft(msg, Promise.all(completion))
|
||||
.then((ctrl) => {
|
||||
.then(ctrl => {
|
||||
if (topic.isArchived()) {
|
||||
topic.archive(false);
|
||||
}
|
||||
return ctrl;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
|
||||
handleNewChatInvitation(topicName, action) {
|
||||
@@ -1091,9 +1087,7 @@ class TinodeWeb extends React.Component {
|
||||
if (topic.isP2PType()) {
|
||||
// For P2P topics change 'given' permission of the peer too.
|
||||
// In p2p topics the other user has the same name as the topic.
|
||||
response = response.then((ctrl) => {
|
||||
topic.setMeta({sub: {user: topicName, mode: mode}});
|
||||
});
|
||||
response = response.then(_ => topic.setMeta({sub: {user: topicName, mode: mode}}));
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
@@ -1104,18 +1098,14 @@ class TinodeWeb extends React.Component {
|
||||
// Ban the topic making futher invites impossible.
|
||||
// Just self-ban.
|
||||
const am = topic.getAccessMode().updateWant('-JP').getWant();
|
||||
response = topic.setMeta({sub: {mode: am}}).then((ctrl) => {
|
||||
return this.handleTopicSelected(null);
|
||||
});
|
||||
response = topic.setMeta({sub: {mode: am}}).then(_ => this.handleTopicSelected(null));
|
||||
break;
|
||||
default:
|
||||
console.warn("Unknown invitation action", '"' + action + '""');
|
||||
}
|
||||
|
||||
if (response != null) {
|
||||
response.catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
response.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1156,7 +1146,7 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
const me = this.tinode.getMeTopic();
|
||||
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.
|
||||
this.setState({incognitoMode: !on});
|
||||
this.handleError(err.message, 'err');
|
||||
@@ -1165,9 +1155,7 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
handleUpdateAccountTagsRequest(tags) {
|
||||
this.tinode.getMeTopic().setMeta({tags: tags})
|
||||
.catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
|
||||
// User chose Settings menu item.
|
||||
@@ -1235,9 +1223,9 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
}
|
||||
} else if (this.state.firebaseToken && this.fcm) {
|
||||
firebaseDelToken(this.fcm).catch((err) => {
|
||||
firebaseDelToken(this.fcm).catch(err => {
|
||||
console.error("Unable to delete token.", err);
|
||||
}).finally(() => {
|
||||
}).finally(_ => {
|
||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
||||
localStorage.removeItem('firebase-token');
|
||||
this.setState({desktopAlerts: false, firebaseToken: null});
|
||||
@@ -1259,16 +1247,14 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
handleCredAdd(method, value) {
|
||||
const me = this.tinode.getMeTopic();
|
||||
me.setMeta({cred: {meth: method, val: value}}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
me.setMeta({cred: {meth: method, val: value}})
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
|
||||
handleCredDelete(method, value) {
|
||||
const me = this.tinode.getMeTopic();
|
||||
me.delCredential(method, value).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
me.delCredential(method, value)
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
|
||||
handleCredConfirm(method, response) {
|
||||
@@ -1332,7 +1318,7 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
}
|
||||
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.
|
||||
@@ -1377,18 +1363,15 @@ class TinodeWeb extends React.Component {
|
||||
if (defacs) {
|
||||
params.defacs = defacs;
|
||||
}
|
||||
topic.setMeta({desc: params, attachments: attachments}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
topic.setMeta({desc: params, attachments: attachments})
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
}
|
||||
|
||||
handleUnarchive(topicName) {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
if (topic) {
|
||||
topic.archive(false).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
topic.archive(false).catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1396,9 +1379,8 @@ class TinodeWeb extends React.Component {
|
||||
this.handleError();
|
||||
|
||||
if (password) {
|
||||
this.tinode.updateAccountBasic(null, this.tinode.getCurrentLogin(), password).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
this.tinode.updateAccountBasic(null, this.tinode.getCurrentLogin(), password)
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1413,18 +1395,16 @@ class TinodeWeb extends React.Component {
|
||||
am.updateWant(mode);
|
||||
mode = am.getWant();
|
||||
}
|
||||
topic.setMeta({sub: {user: uid, mode: mode}}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
topic.setMeta({sub: {user: uid, mode: mode}})
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
}
|
||||
|
||||
handleTagsUpdateRequest(topicName, tags) {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
if (topic) {
|
||||
topic.setMeta({tags: tags}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
topic.setMeta({tags: tags})
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1451,9 +1431,9 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
this.setState(this.getBlankState());
|
||||
|
||||
cleared.then(() => {
|
||||
cleared.then(_ => {
|
||||
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.onDisconnect = this.handleDisconnect;
|
||||
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
|
||||
@@ -1477,10 +1457,10 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
// Request to hard-delete topic.
|
||||
topic.delTopic(true).then((ctrl) => {
|
||||
topic.delTopic(true).then(_ => {
|
||||
// Hide MessagesView and InfoView panels.
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||
}).catch((err) => {
|
||||
}).catch(err => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
@@ -1492,9 +1472,8 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
// Request hard-delete all messages.
|
||||
topic.delMessagesAll(true).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
topic.delMessagesAll(true)
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
|
||||
handleLeaveUnsubRequest(topicName) {
|
||||
@@ -1503,10 +1482,10 @@ class TinodeWeb extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.leave(true).then((ctrl) => {
|
||||
topic.leave(true).then(_ => {
|
||||
// Hide MessagesView and InfoView panels.
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||
}).catch((err) => {
|
||||
}).catch(err => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
@@ -1517,12 +1496,12 @@ class TinodeWeb extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.updateMode(null, '-JP').then(_ => {
|
||||
// Hide MessagesView and InfoView panels.
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||
}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
topic.updateMode(null, '-JP')
|
||||
.then(_ => {
|
||||
// Hide MessagesView and InfoView panels.
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||
})
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
|
||||
handleReportTopic(topicName) {
|
||||
@@ -1535,12 +1514,12 @@ class TinodeWeb extends React.Component {
|
||||
this.tinode.report('report', topicName);
|
||||
|
||||
// Remove J and P permissions.
|
||||
topic.updateMode(null, '-JP').then((ctrl) => {
|
||||
topic.updateMode(null, '-JP')
|
||||
.then(_ => {
|
||||
// Hide MessagesView and InfoView panels.
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||
}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
})
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
|
||||
handleShowContextMenu(params, menuItems) {
|
||||
@@ -1637,7 +1616,7 @@ class TinodeWeb extends React.Component {
|
||||
handleContextMenuAction(action, promise, params) {
|
||||
if (action == 'topic_archive') {
|
||||
if (promise && params.topicName && params.topicName == this.state.topicSelected) {
|
||||
promise.then(() => {
|
||||
promise.then(_ => {
|
||||
this.handleTopicSelected(null);
|
||||
});
|
||||
}
|
||||
@@ -1677,17 +1656,15 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
if (added && added.length > 0) {
|
||||
added.map((uid) => {
|
||||
topic.invite(uid, null).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
topic.invite(uid, null)
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
});
|
||||
}
|
||||
|
||||
if (removed && removed.length > 0) {
|
||||
removed.map((uid) => {
|
||||
topic.delSubscription(uid).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
topic.delSubscription(uid)
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1696,9 +1673,7 @@ class TinodeWeb extends React.Component {
|
||||
if (this.tinode.isAuthenticated()) {
|
||||
// Adding new email or phone number in account setting.
|
||||
this.tinode.getMeTopic().setMeta({cred: {meth: cred, resp: code}})
|
||||
.catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
} else {
|
||||
// Credential validation on signup.
|
||||
this.setState({credMethod: cred, credCode: code, credToken: token});
|
||||
@@ -1709,10 +1684,8 @@ class TinodeWeb extends React.Component {
|
||||
handlePasswordResetRequest(method, value) {
|
||||
// If already connected, connnect() will return a resolved promise.
|
||||
return this.tinode.connect()
|
||||
.then(() => {
|
||||
return this.tinode.requestResetAuthSecret('basic', method, value);
|
||||
})
|
||||
.catch((err) => {
|
||||
.then(_ => this.tinode.requestResetAuthSecret('basic', method, value))
|
||||
.catch(err => {
|
||||
// Socket error
|
||||
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');
|
||||
} else {
|
||||
this.tinode.connect()
|
||||
.then(() => {
|
||||
return this.tinode.updateAccountBasic(null, null, newPassword, {token: token});
|
||||
})
|
||||
.then(() => {
|
||||
HashNavigation.navigateTo('');
|
||||
})
|
||||
.catch((err) => {
|
||||
.then(_ => this.tinode.updateAccountBasic(null, null, newPassword, {token: token}))
|
||||
.then(_ => HashNavigation.navigateTo(''))
|
||||
.catch(err => {
|
||||
// Socket error
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
@@ -1749,7 +1718,7 @@ class TinodeWeb extends React.Component {
|
||||
case CALL_STATE_OUTGOING_INITATED:
|
||||
let head = { webrtc: CALL_HEAD_STARTED };
|
||||
this.handleSendMessage(Drafty.videoCall(), undefined, undefined, head)
|
||||
.then((ctrl) => {
|
||||
.then(ctrl => {
|
||||
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
|
||||
this.handleCallClose();
|
||||
return;
|
||||
@@ -1836,7 +1805,7 @@ class TinodeWeb extends React.Component {
|
||||
// We need to switch and subscribe to callTopic first.
|
||||
this.setState({
|
||||
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}
|
||||
title={this.state.alertParams.title}
|
||||
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}
|
||||
onConfirm={() => { this.setState({alertVisible: false}); this.state.alertParams.onConfirm(); }}
|
||||
onConfirm={_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}}
|
||||
confirm={this.state.alertParams.confirm}
|
||||
/>
|
||||
<SidepanelView
|
||||
|
||||
@@ -27,15 +27,11 @@ export default class Attachment extends React.Component {
|
||||
const downloader = this.props.tinode.getLargeFileHelper();
|
||||
this.setState({downloader: downloader});
|
||||
downloader.download(url, filename, mimetype,
|
||||
(loaded) => {
|
||||
this.setState({progress: loaded / this.props.size});
|
||||
},
|
||||
(err) => {
|
||||
this.props.onError(err, 'err');
|
||||
}
|
||||
).then(() => {
|
||||
this.setState({downloader: null, progress: 0});
|
||||
}).catch((err) => {
|
||||
loaded => this.setState({progress: loaded / this.props.size}),
|
||||
err => this.props.onError(err, 'err')
|
||||
)
|
||||
.then(_ => this.setState({downloader: null, progress: 0}))
|
||||
.catch(err => {
|
||||
if (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;
|
||||
|
||||
const drawFrame = () => {
|
||||
const drawFrame = _ => {
|
||||
if (!this.canvasRef.current || !this.audioPlayer) {
|
||||
// The component is unmounted.
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,8 @@ import React from 'react';
|
||||
|
||||
import AudioPlayer from './audio-player.jsx';
|
||||
// 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 { secondsToTime } from '../lib/strformat';
|
||||
@@ -105,7 +106,7 @@ export default class AudioRecorder extends React.PureComponent {
|
||||
let prevBarCount = 0;
|
||||
let volume = 0.0;
|
||||
let countPerBar = 0;
|
||||
const drawFrame = () => {
|
||||
const drawFrame = _ => {
|
||||
if (!this.startedOn) {
|
||||
return;
|
||||
}
|
||||
@@ -226,7 +227,7 @@ export default class AudioRecorder extends React.PureComponent {
|
||||
|
||||
initMediaRecording(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.
|
||||
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.
|
||||
// If duration is valid, apply fix for Chrome's WebM duration bug.
|
||||
getRecording(mimeType, duration) {
|
||||
let blob = new Blob(this.audioChunks, {type: mimeType || AUDIO_MIME_TYPE});
|
||||
return (duration > 0 ? fixWebmDuration(blob, duration, {logger: false}) : Promise.resolve(blob))
|
||||
mimeType = mimeType || AUDIO_MIME_TYPE;
|
||||
let blob = new Blob(this.audioChunks, {type: mimeType});
|
||||
return fixWebmDuration(blob, mimeType)
|
||||
.then(fixedBlob => { blob = fixedBlob; return fixedBlob.arrayBuffer(); })
|
||||
.then(arrayBuff => this.audioContext.decodeAudioData(arrayBuff))
|
||||
.then(decoded => this.createPreview(decoded))
|
||||
|
||||
@@ -231,7 +231,7 @@ class CallPanel extends React.PureComponent {
|
||||
const desc = new RTCSessionDescription(info.payload);
|
||||
this.state.pc.setRemoteDescription(desc)
|
||||
.then(_ => {
|
||||
this.setState({ callInitialSetupComplete: true }, () => this.drainRemoteIceCandidatesCache());
|
||||
this.setState({ callInitialSetupComplete: true }, _ => this.drainRemoteIceCandidatesCache());
|
||||
})
|
||||
.catch(this.reportError);
|
||||
}
|
||||
@@ -357,7 +357,7 @@ class CallPanel extends React.PureComponent {
|
||||
})
|
||||
.then(_ => {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ class BaseChatMessage extends React.PureComponent {
|
||||
|
||||
this.props.showContextMenu({
|
||||
seq: this.props.seq,
|
||||
replace: this.props.edited ? parseInt(this.props.edited.split(':')[1]) : 0,
|
||||
content: this.props.content,
|
||||
userFrom: this.props.userFrom,
|
||||
userName: this.props.userName,
|
||||
|
||||
@@ -196,7 +196,7 @@ class ContextMenu extends React.Component {
|
||||
'menu_item_forward': {
|
||||
id: 'menu_item_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': {
|
||||
id: 'menu_item_edit',
|
||||
@@ -227,12 +227,10 @@ class ContextMenu extends React.Component {
|
||||
return props.onShowAlert(
|
||||
formatMessage(messages.block), // title
|
||||
formatMessage(messages.topic_block_warning), // content
|
||||
(() => {
|
||||
return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => {
|
||||
this.props.onTopicRemoved(params.topicName);
|
||||
return ctrl;
|
||||
});
|
||||
}),
|
||||
(_ => this.topicPermissionSetter('-JP', params, errorHandler).then(ctrl => {
|
||||
this.props.onTopicRemoved(params.topicName);
|
||||
return ctrl;
|
||||
})),
|
||||
null, // "OK"
|
||||
true, // Show Reject button
|
||||
null // "Cancel"
|
||||
@@ -246,13 +244,13 @@ class ContextMenu extends React.Component {
|
||||
return props.onShowAlert(
|
||||
formatMessage(messages.topic_delete), // title
|
||||
formatMessage(messages.topic_delete_warning), // content
|
||||
(() => {
|
||||
(_ => {
|
||||
const topic = this.props.tinode.getTopic(params.topicName);
|
||||
if (!topic) {
|
||||
console.warn("Topic not found: ", params.topicName);
|
||||
return;
|
||||
}
|
||||
return topic.delTopic(true).catch((err) => {
|
||||
return topic.delTopic(true).catch(err => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
@@ -273,7 +271,7 @@ class ContextMenu extends React.Component {
|
||||
console.warn("Topic not found: ", params.topicName);
|
||||
return;
|
||||
}
|
||||
return topic.archive(true).catch((err) => {
|
||||
return topic.archive(true).catch(err => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
@@ -289,7 +287,7 @@ class ContextMenu extends React.Component {
|
||||
console.warn("Topic not found: ", params.topicName);
|
||||
return;
|
||||
}
|
||||
return topic.archive(false).catch((err) => {
|
||||
return topic.archive(false).catch(err => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
@@ -311,7 +309,7 @@ class ContextMenu extends React.Component {
|
||||
console.warn("Topic or user not found: '" + params.topicName + "', '" + params.user + "'");
|
||||
return;
|
||||
}
|
||||
return topic.delSubscription(params.user).catch((err) => {
|
||||
return topic.delSubscription(params.user).catch(err => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
@@ -401,12 +399,14 @@ class ContextMenu extends React.Component {
|
||||
if (!all && topic.cancelSend(params.seq)) {
|
||||
return;
|
||||
}
|
||||
// Can't cancel. Delete instead.
|
||||
|
||||
const promise = all ?
|
||||
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) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
@@ -421,7 +421,7 @@ class ContextMenu extends React.Component {
|
||||
return;
|
||||
}
|
||||
const msg = topic.createMessage(params.content, false);
|
||||
return topic.publishDraft(msg).catch((err) => {
|
||||
return topic.publishDraft(msg).catch(err => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
@@ -438,9 +438,7 @@ class ContextMenu extends React.Component {
|
||||
|
||||
let result = topic.updateMode(params.user, mode);
|
||||
if (errorHandler) {
|
||||
result = result.catch((err) => {
|
||||
errorHandler(err.message, 'err');
|
||||
});
|
||||
result = result.catch(err => errorHandler(err.message, 'err'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -450,7 +448,7 @@ class ContextMenu extends React.Component {
|
||||
}
|
||||
|
||||
editMessage(params, errorHandler) {
|
||||
params.editMessage(params.seq, params.content, errorHandler);
|
||||
params.editMessage(params.replace || params.seq, params.content, errorHandler);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class InPlaceEdit extends React.Component {
|
||||
|
||||
handleStartEditing() {
|
||||
if (!this.props.readOnly) {
|
||||
this.setState({active: true}, () => {
|
||||
this.setState({active: true}, _ => {
|
||||
if (this.selfRef.current) {
|
||||
this.selfRef.current.focus();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Image with a placeholder which is replaced when the promise is resolved.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class LazyImage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -20,7 +19,7 @@ export default class LazyImage extends React.PureComponent {
|
||||
this.props.whenDone
|
||||
.promise
|
||||
.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() {
|
||||
@@ -33,7 +32,7 @@ export default class LazyImage extends React.PureComponent {
|
||||
this.props.whenDone
|
||||
.promise
|
||||
.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 { FormattedMessage } from 'react-intl';
|
||||
|
||||
import AvatarCrop from './avatar-crop.jsx';
|
||||
import AvatarUpload from './avatar-upload.jsx';
|
||||
import CheckBox from './checkbox.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 {
|
||||
constructor(props) {
|
||||
@@ -17,13 +20,18 @@ export default class NewTopicGroup extends React.PureComponent {
|
||||
fullName: '', // full/formatted name
|
||||
private: '',
|
||||
description: '',
|
||||
imageDataUrl: null,
|
||||
imageUrl: null,
|
||||
tags: [],
|
||||
isChannel: false
|
||||
isChannel: false,
|
||||
newAvatar: null,
|
||||
newAvatarMime: null
|
||||
};
|
||||
|
||||
this.handleFieldEdit = this.handleFieldEdit.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.handleChannelToggle = this.handleChannelToggle.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
@@ -38,7 +46,47 @@ export default class NewTopicGroup extends React.PureComponent {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -56,11 +104,22 @@ export default class NewTopicGroup extends React.PureComponent {
|
||||
const comment = this.state.private.trim().substring(0, MAX_TITLE_LENGTH);
|
||||
const description = this.state.description.trim().substring(0, MAX_TOPIC_DESCRIPTION_LENGTH);
|
||||
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() {
|
||||
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';
|
||||
if (this.props.disabled) {
|
||||
submitClasses += ' disabled';
|
||||
@@ -71,6 +130,7 @@ export default class NewTopicGroup extends React.PureComponent {
|
||||
<center>
|
||||
<AvatarUpload
|
||||
tinode={this.props.tinode}
|
||||
avatar={this.state.imageUrl}
|
||||
onError={this.props.onError}
|
||||
onImageUpdated={this.handleImageChanged} />
|
||||
</center>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Send message form.
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
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 { filePasted } from '../lib/blob-helpers.js';
|
||||
@@ -231,9 +233,11 @@ class SendMessage extends React.PureComponent {
|
||||
{this.props.noInput ?
|
||||
(quote || <div className="hr thin" />) :
|
||||
(this.state.audioRec ?
|
||||
<AudioRecorder
|
||||
onDeleted={_ => this.setState({audioRec: false})}
|
||||
onFinished={this.handleAttachAudio}/> :
|
||||
(<Suspense fallback={<div>Loading...</div>}>
|
||||
<AudioRecorder
|
||||
onDeleted={_ => this.setState({audioRec: false})}
|
||||
onFinished={this.handleAttachAudio}/>
|
||||
</Suspense>) :
|
||||
<textarea id="sendMessage" placeholder={prompt}
|
||||
value={this.state.message}
|
||||
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
|
||||
// for uploading it to the server out of band.
|
||||
uploadAvatar(mime, blob, width, height) {
|
||||
const readyToUpload = (image) => {
|
||||
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.
|
||||
|
||||
@@ -91,7 +91,7 @@ class TopicSecurity extends React.PureComponent {
|
||||
this.props.onShowAlert(
|
||||
formatMessage(messages.topic_delete), // title
|
||||
formatMessage(messages.topic_delete_warning), // content
|
||||
(() => { this.props.onDeleteTopic(this.props.topic); }), // onConfirm
|
||||
_ => this.props.onDeleteTopic(this.props.topic), // onConfirm
|
||||
null, // "OK"
|
||||
true, // Show Reject button
|
||||
null // "Cancel"
|
||||
@@ -104,7 +104,7 @@ class TopicSecurity extends React.PureComponent {
|
||||
this.props.onShowAlert(
|
||||
formatMessage(this.props.deleter ? messages.delete_messages : messages.clear_messages), // title
|
||||
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"
|
||||
true, // Show Reject button
|
||||
null // "Cancel"
|
||||
@@ -117,7 +117,7 @@ class TopicSecurity extends React.PureComponent {
|
||||
this.props.onShowAlert(
|
||||
formatMessage(messages.leave_chat), // title
|
||||
formatMessage(messages.leave_chat_warning), // content
|
||||
(() => { this.props.onLeaveTopic(this.props.topic); }), // onConfirm
|
||||
_ => this.props.onLeaveTopic(this.props.topic), // onConfirm
|
||||
null, // "OK"
|
||||
true, // Show Reject button
|
||||
null // "Cancel"
|
||||
@@ -130,7 +130,7 @@ class TopicSecurity extends React.PureComponent {
|
||||
this.props.onShowAlert(
|
||||
formatMessage(messages.block_contact), // title
|
||||
formatMessage(messages.block_contact_warning), // content
|
||||
(() => { this.props.onBlockTopic(this.props.topic); }), // onConfirm
|
||||
_ => this.props.onBlockTopic(this.props.topic), // onConfirm
|
||||
null, // "OK"
|
||||
true, // Show Reject button
|
||||
null // "Cancel"
|
||||
|
||||
@@ -52,7 +52,7 @@ export default class VisiblePassword extends React.PureComponent {
|
||||
handleEditingFinished(e) {
|
||||
if (e) {
|
||||
let currentTarget = e.currentTarget;
|
||||
setTimeout(() => {
|
||||
setTimeout(_ => {
|
||||
if (!currentTarget.contains(document.activeElement)) {
|
||||
if (this.props.onFinished) {
|
||||
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