Merge branch 'develop' into travis/stickerpicker/remount

This commit is contained in:
Travis Ralston 2019-04-03 17:00:19 -06:00
commit ad777782b8
18 changed files with 521 additions and 236 deletions

View file

@ -52,7 +52,6 @@ src/components/views/settings/ChangePassword.js
src/components/views/settings/DevicesPanel.js src/components/views/settings/DevicesPanel.js
src/components/views/settings/IntegrationsManager.js src/components/views/settings/IntegrationsManager.js
src/components/views/settings/Notifications.js src/components/views/settings/Notifications.js
src/ContentMessages.js
src/GroupAddressPicker.js src/GroupAddressPicker.js
src/HtmlUtils.js src/HtmlUtils.js
src/ImageUtils.js src/ImageUtils.js

View file

@ -70,6 +70,7 @@
@import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_SettingsDialog.scss";
@import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss"; @import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";

View file

@ -0,0 +1,33 @@
/*
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_UploadConfirmDialog_fileIcon {
margin-right: 5px;
}
.mx_UploadConfirmDialog_previewOuter {
text-align: center;
}
.mx_UploadConfirmDialog_previewInner {
display: inline-block;
text-align: left;
}
.mx_UploadConfirmDialog_imagePreview {
max-height: 300px;
max-width: 100%;
}

View file

@ -47,6 +47,9 @@ limitations under the License.
transform: scale(0); transform: scale(0);
} }
.mx_RoomBreadcrumbs_left {
opacity: 0.5;
}
// Note: we have to manually control the gradient and stuff, but the IndicatorScrollbar // Note: we have to manually control the gradient and stuff, but the IndicatorScrollbar
// will deal with left/right positioning for us. Normally we'd use position:sticky on // will deal with left/right positioning for us. Normally we'd use position:sticky on

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,17 +18,18 @@ limitations under the License.
'use strict'; 'use strict';
import Promise from 'bluebird'; import Promise from 'bluebird';
const extend = require('./extend'); import extend from './extend';
const dis = require('./dispatcher'); import dis from './dispatcher';
const MatrixClientPeg = require('./MatrixClientPeg'); import MatrixClientPeg from './MatrixClientPeg';
const sdk = require('./index'); import sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
const Modal = require('./Modal'); import Modal from './Modal';
import RoomViewStore from './stores/RoomViewStore';
const encrypt = require("browser-encrypt-attachment"); import encrypt from "browser-encrypt-attachment";
// Polyfill for Canvas.toBlob API using Canvas.toDataURL // Polyfill for Canvas.toBlob API using Canvas.toDataURL
require("blueimp-canvas-to-blob"); import "blueimp-canvas-to-blob";
const MAX_WIDTH = 800; const MAX_WIDTH = 800;
const MAX_HEIGHT = 600; const MAX_HEIGHT = 600;
@ -91,7 +93,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
/** /**
* Load a file into a newly created image element. * Load a file into a newly created image element.
* *
* @param {File} file The file to load in an image element. * @param {File} imageFile The file to load in an image element.
* @return {Promise} A promise that resolves with the html image element. * @return {Promise} A promise that resolves with the html image element.
*/ */
function loadImageElement(imageFile) { function loadImageElement(imageFile) {
@ -119,7 +121,7 @@ function loadImageElement(imageFile) {
* *
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with. * @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
* @param {String} roomId The ID of the room the image will be uploaded in. * @param {String} roomId The ID of the room the image will be uploaded in.
* @param {File} The image to read and thumbnail. * @param {File} imageFile The image to read and thumbnail.
* @return {Promise} A promise that resolves with the attachment info. * @return {Promise} A promise that resolves with the attachment info.
*/ */
function infoForImageFile(matrixClient, roomId, imageFile) { function infoForImageFile(matrixClient, roomId, imageFile) {
@ -144,7 +146,7 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
/** /**
* Load a file into a newly created video element. * Load a file into a newly created video element.
* *
* @param {File} file The file to load in an video element. * @param {File} videoFile The file to load in an video element.
* @return {Promise} A promise that resolves with the video image element. * @return {Promise} A promise that resolves with the video image element.
*/ */
function loadVideoElement(videoFile) { function loadVideoElement(videoFile) {
@ -179,7 +181,7 @@ function loadVideoElement(videoFile) {
* *
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with. * @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
* @param {String} roomId The ID of the room the video will be uploaded to. * @param {String} roomId The ID of the room the video will be uploaded to.
* @param {File} The video to read and thumbnail. * @param {File} videoFile The video to read and thumbnail.
* @return {Promise} A promise that resolves with the attachment info. * @return {Promise} A promise that resolves with the attachment info.
*/ */
function infoForVideoFile(matrixClient, roomId, videoFile) { function infoForVideoFile(matrixClient, roomId, videoFile) {
@ -200,6 +202,7 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
/** /**
* Read the file as an ArrayBuffer. * Read the file as an ArrayBuffer.
* @param {File} file The file to read
* @return {Promise} A promise that resolves with an ArrayBuffer when the file * @return {Promise} A promise that resolves with an ArrayBuffer when the file
* is read. * is read.
*/ */
@ -269,11 +272,43 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
} }
} }
export default class ContentMessages {
class ContentMessages {
constructor() { constructor() {
this.inprogress = []; this.inprogress = [];
this.nextId = 0; this.nextId = 0;
this._mediaConfig = null;
}
static sharedInstance() {
if (global.mx_ContentMessages === undefined) {
global.mx_ContentMessages = new ContentMessages();
}
return global.mx_ContentMessages;
}
_isFileSizeAcceptable(file) {
if (this._mediaConfig !== null &&
this._mediaConfig["m.upload.size"] !== undefined &&
file.size > this._mediaConfig["m.upload.size"]) {
return false;
}
return true;
}
_ensureMediaConfigFetched() {
if (this._mediaConfig !== null) return;
console.log("[Media Config] Fetching");
return MatrixClientPeg.get().getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits).
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {};
}).then((config) => {
this._mediaConfig = config;
});
} }
sendStickerContentToRoom(url, roomId, info, text, matrixClient) { sendStickerContentToRoom(url, roomId, info, text, matrixClient) {
@ -283,7 +318,90 @@ class ContentMessages {
}); });
} }
sendContentToRoom(file, roomId, matrixClient) { getUploadLimit() {
if (this._mediaConfig !== null && this._mediaConfig["m.upload.size"] !== undefined) {
return this._mediaConfig["m.upload.size"];
} else {
return null;
}
}
async sendContentListToRoom(files, roomId, matrixClient) {
if (matrixClient.isGuest()) {
dis.dispatch({action: 'require_registration'});
return;
}
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (isQuoting) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const shouldUpload = await new Promise((resolve) => {
Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, {
title: _t('Replying With Files'),
description: (
<div>{_t(
'At this time it is not possible to reply with a file. ' +
'Would you like to upload this file without replying?',
)}</div>
),
hasCancelButton: true,
button: _t("Continue"),
onFinished: (shouldUpload) => {
resolve(shouldUpload);
},
});
});
if (!shouldUpload) return;
}
await this._ensureMediaConfigFetched();
const tooBigFiles = [];
const okFiles = [];
for (let i = 0; i < files.length; ++i) {
if (this._isFileSizeAcceptable(files[i])) {
okFiles.push(files[i]);
} else {
tooBigFiles.push(files[i]);
}
}
if (tooBigFiles.length > 0) {
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
const uploadFailureDialogPromise = new Promise((resolve) => {
Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, {
badFiles: tooBigFiles,
totalFiles: files.length,
contentMessages: this,
onFinished: (shouldContinue) => {
resolve(shouldContinue);
},
});
});
const shouldContinue = await uploadFailureDialogPromise;
if (!shouldContinue) return;
}
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i];
const shouldContinue = await new Promise((resolve) => {
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
file,
currentIndex: i,
totalFiles: okFiles.length,
onFinished: (shouldContinue) => {
resolve(shouldContinue);
},
});
});
if (!shouldContinue) break;
this._sendContentToRoom(file, roomId, matrixClient);
}
}
_sendContentToRoom(file, roomId, matrixClient) {
const content = { const content = {
body: file.name || 'Attachment', body: file.name || 'Attachment',
info: { info: {
@ -357,9 +475,12 @@ class ContentMessages {
}, function(err) { }, function(err) {
error = err; error = err;
if (!upload.canceled) { if (!upload.canceled) {
let desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.'; let desc = _t("The file '%(fileName)s' failed to upload.", {fileName: upload.fileName});
if (err.http_status == 413) { if (err.http_status == 413) {
desc = _t('The file \'%(fileName)s\' exceeds this homeserver\'s size limit for uploads', {fileName: upload.fileName}); desc = _t(
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
{fileName: upload.fileName},
);
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, { Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
@ -377,9 +498,16 @@ class ContentMessages {
} }
} }
if (error) { if (error) {
// 413: File was too big or upset the server in some way:
// clear the media size limit so we fetch it again next time
// we try to upload
if (error && error.http_status === 413) {
this._mediaConfig = null;
}
dis.dispatch({action: 'upload_failed', upload, error}); dis.dispatch({action: 'upload_failed', upload, error});
} else { } else {
dis.dispatch({action: 'upload_finished', upload}); dis.dispatch({action: 'upload_finished', upload});
dis.dispatch({action: 'message_sent'});
} }
}); });
} }
@ -404,9 +532,3 @@ class ContentMessages {
} }
} }
} }
if (global.mx_ContentMessage === undefined) {
global.mx_ContentMessage = new ContentMessages();
}
module.exports = global.mx_ContentMessage;

View file

@ -120,7 +120,7 @@ class MatrixClientPeg {
await this.matrixClient.initCrypto(); await this.matrixClient.initCrypto();
} }
} catch (e) { } catch (e) {
if (e.name === 'InvalidCryptoStoreError') { if (e && e.name === 'InvalidCryptoStoreError') {
// The js-sdk found a crypto DB too new for it to use // The js-sdk found a crypto DB too new for it to use
const CryptoStoreTooNewDialog = const CryptoStoreTooNewDialog =
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
@ -130,7 +130,7 @@ class MatrixClientPeg {
} }
// this can happen for a number of reasons, the most likely being // this can happen for a number of reasons, the most likely being
// that the olm library was missing. It's not fatal. // that the olm library was missing. It's not fatal.
console.warn("Unable to initialise e2e: " + e); console.warn("Unable to initialise e2e", e);
} }
const opts = utils.deepCopy(this.opts); const opts = utils.deepCopy(this.opts);

View file

@ -56,6 +56,7 @@ export default class ContextualMenu extends React.Component {
menuPaddingRight: PropTypes.number, menuPaddingRight: PropTypes.number,
menuPaddingBottom: PropTypes.number, menuPaddingBottom: PropTypes.number,
menuPaddingLeft: PropTypes.number, menuPaddingLeft: PropTypes.number,
zIndex: PropTypes.number,
// If true, insert an invisible screen-sized element behind the // If true, insert an invisible screen-sized element behind the
// menu that when clicked will close it. // menu that when clicked will close it.
@ -215,16 +216,22 @@ export default class ContextualMenu extends React.Component {
menuStyle["paddingRight"] = props.menuPaddingRight; menuStyle["paddingRight"] = props.menuPaddingRight;
} }
const wrapperStyle = {};
if (!isNaN(Number(props.zIndex))) {
menuStyle["zIndex"] = props.zIndex + 1;
wrapperStyle["zIndex"] = props.zIndex;
}
const ElementClass = props.elementClass; const ElementClass = props.elementClass;
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished // FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click! // property set here so you can't close the menu from a button click!
return <div className={className} style={position}> return <div className={className} style={{...position, ...wrapperStyle}}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect}> <div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect}>
{ chevron } { chevron }
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} /> <ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
</div> </div>
{ props.hasBackground && <div className="mx_ContextualMenu_background" { props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> } onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
<style>{ chevronCSS }</style> <style>{ chevronCSS }</style>
</div>; </div>;

View file

@ -19,29 +19,28 @@ limitations under the License.
// TODO: This component is enormous! There's several things which could stand-alone: // TODO: This component is enormous! There's several things which could stand-alone:
// - Search results component // - Search results component
// - Drag and drop // - Drag and drop
// - File uploading - uploadFile()
import shouldHideEvent from "../../shouldHideEvent"; import shouldHideEvent from '../../shouldHideEvent';
const React = require("react"); import React from 'react';
const ReactDOM = require("react-dom"); import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Promise from 'bluebird'; import Promise from 'bluebird';
import filesize from 'filesize'; import filesize from 'filesize';
const classNames = require("classnames"); import classNames from 'classnames';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import {RoomPermalinkCreator} from "../../matrix-to"; import {RoomPermalinkCreator} from '../../matrix-to';
const MatrixClientPeg = require("../../MatrixClientPeg"); import MatrixClientPeg from '../../MatrixClientPeg';
const ContentMessages = require("../../ContentMessages"); import ContentMessages from '../../ContentMessages';
const Modal = require("../../Modal"); import Modal from '../../Modal';
const sdk = require('../../index'); import sdk from '../../index';
const CallHandler = require('../../CallHandler'); import CallHandler from '../../CallHandler';
const dis = require("../../dispatcher"); import dis from '../../dispatcher';
const Tinter = require("../../Tinter"); import Tinter from '../../Tinter';
const rate_limited_func = require('../../ratelimitedfunc'); import rate_limited_func from '../../ratelimitedfunc';
const ObjectUtils = require('../../ObjectUtils'); import ObjectUtils from '../../ObjectUtils';
const Rooms = require('../../Rooms'); import Rooms from '../../Rooms';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
@ -170,7 +169,6 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("accountData", this.onAccountData); MatrixClientPeg.get().on("accountData", this.onAccountData);
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus); MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged); MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this._fetchMediaConfig();
// Start listening for RoomViewStore updates // Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true); this._onRoomViewStoreUpdate(true);
@ -178,27 +176,6 @@ module.exports = React.createClass({
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate); WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
}, },
_fetchMediaConfig: function(invalidateCache: boolean = false) {
/// NOTE: Using global here so we don't make repeated requests for the
/// config every time we swap room.
if(global.mediaConfig !== undefined && !invalidateCache) {
this.setState({mediaConfig: global.mediaConfig});
return;
}
console.log("[Media Config] Fetching");
MatrixClientPeg.get().getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits).
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {};
}).then((config) => {
global.mediaConfig = config;
this.setState({mediaConfig: config});
});
},
_onRoomViewStoreUpdate: function(initial) { _onRoomViewStoreUpdate: function(initial) {
if (this.unmounted) { if (this.unmounted) {
return; return;
@ -510,7 +487,7 @@ module.exports = React.createClass({
}, },
onPageUnload(event) { onPageUnload(event) {
if (ContentMessages.getCurrentUploads().length > 0) { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
return event.returnValue = return event.returnValue =
_t("You seem to be uploading files, are you sure you want to quit?"); _t("You seem to be uploading files, are you sure you want to quit?");
} else if (this._getCallForRoom() && this.state.callState !== 'ended') { } else if (this._getCallForRoom() && this.state.callState !== 'ended') {
@ -561,11 +538,6 @@ module.exports = React.createClass({
case 'picture_snapshot': case 'picture_snapshot':
this.uploadFile(payload.file); this.uploadFile(payload.file);
break; break;
case 'upload_failed':
// 413: File was too big or upset the server in some way.
if (payload.error && payload.error.http_status === 413) {
this._fetchMediaConfig(true);
}
case 'notifier_enabled': case 'notifier_enabled':
case 'upload_started': case 'upload_started':
case 'upload_finished': case 'upload_finished':
@ -1015,9 +987,11 @@ module.exports = React.createClass({
onDrop: function(ev) { onDrop: function(ev) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
ContentMessages.sharedInstance().sendContentListToRoom(
ev.dataTransfer.files, this.state.room.roomId, MatrixClientPeg.get(),
);
this.setState({ draggingFile: false }); this.setState({ draggingFile: false });
const files = [...ev.dataTransfer.files]; dis.dispatch({action: 'focus_composer'});
files.forEach(this.uploadFile);
}, },
onDragLeaveOrEnd: function(ev) { onDragLeaveOrEnd: function(ev) {
@ -1026,55 +1000,13 @@ module.exports = React.createClass({
this.setState({ draggingFile: false }); this.setState({ draggingFile: false });
}, },
isFileUploadAllowed(file) {
if (this.state.mediaConfig !== undefined &&
this.state.mediaConfig["m.upload.size"] !== undefined &&
file.size > this.state.mediaConfig["m.upload.size"]) {
return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.state.mediaConfig["m.upload.size"])});
}
return true;
},
uploadFile: async function(file) {
dis.dispatch({action: 'focus_composer'});
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
return;
}
try {
await ContentMessages.sendContentToRoom(file, this.state.room.roomId, MatrixClientPeg.get());
} catch (error) {
if (error.name === "UnknownDeviceError") {
// Let the status bar handle this
return;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to upload file " + file + " " + error);
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
title: _t('Failed to upload file'),
description: ((error && error.message)
? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
});
// bail early to avoid calling the dispatch below
return;
}
// Send message_sent callback, for things like _checkIfAlone because after all a file is still a message.
dis.dispatch({
action: 'message_sent',
});
},
injectSticker: function(url, info, text) { injectSticker: function(url, info, text) {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'}); dis.dispatch({action: 'require_registration'});
return; return;
} }
ContentMessages.sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
.done(undefined, (error) => { .done(undefined, (error) => {
if (error.name === "UnknownDeviceError") { if (error.name === "UnknownDeviceError") {
// Let the staus bar handle this // Let the staus bar handle this
@ -1666,7 +1598,7 @@ module.exports = React.createClass({
let statusBar; let statusBar;
let isStatusAreaExpanded = true; let isStatusAreaExpanded = true;
if (ContentMessages.getCurrentUploads().length > 0) { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
const UploadBar = sdk.getComponent('structures.UploadBar'); const UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />; statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) { } else if (!this.state.searchResults) {
@ -1774,11 +1706,9 @@ module.exports = React.createClass({
messageComposer = messageComposer =
<MessageComposer <MessageComposer
room={this.state.room} room={this.state.room}
uploadFile={this.uploadFile}
callState={this.state.callState} callState={this.state.callState}
disabled={this.props.disabled} disabled={this.props.disabled}
showApps={this.state.showApps} showApps={this.state.showApps}
uploadAllowed={this.isFileUploadAllowed}
e2eStatus={this.state.e2eStatus} e2eStatus={this.state.e2eStatus}
permalinkCreator={this.state.permalinkCreator} permalinkCreator={this.state.permalinkCreator}
/>; />;

View file

@ -29,8 +29,11 @@ const UNPAGINATION_PADDING = 6000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent // The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests. // many scroll events causing many unfilling requests.
const UNFILL_REQUEST_DEBOUNCE_MS = 200; const UNFILL_REQUEST_DEBOUNCE_MS = 200;
// _updateHeight makes the height a ceiled multiple of this so we
const PAGE_SIZE = 200; // don't have to update the height too often. It also allows the user
// to scroll past the pagination spinner a bit so they don't feel blocked so
// much while the content loads.
const PAGE_SIZE = 400;
let debuglog; let debuglog;
if (DEBUG_SCROLL) { if (DEBUG_SCROLL) {
@ -222,10 +225,12 @@ module.exports = React.createClass({
// whether it will stay that way when the children update. // whether it will stay that way when the children update.
isAtBottom: function() { isAtBottom: function() {
const sn = this._getScrollNode(); const sn = this._getScrollNode();
// fractional values for scrollTop happen on certain browsers/platforms // fractional values (both too big and too small)
// for scrollTop happen on certain browsers/platforms
// when scrolled all the way down. E.g. Chrome 72 on debian. // when scrolled all the way down. E.g. Chrome 72 on debian.
// so ceil everything upwards to make sure it aligns. // so check difference <= 1;
return Math.ceil(sn.scrollTop) === Math.ceil(sn.scrollHeight - sn.clientHeight); return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
}, },
// returns the vertical height in the given direction that can be removed from // returns the vertical height in the given direction that can be removed from

View file

@ -47,7 +47,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
}, },
render: function() { render: function() {
const uploads = ContentMessages.getCurrentUploads(); const uploads = ContentMessages.sharedInstance().getCurrentUploads();
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length // for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
// check in RoomView // check in RoomView
@ -93,7 +93,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
</div> </div>
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src={require("../../../res/img/fileicon.png")} width="17" height="22" /> <img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src={require("../../../res/img/fileicon.png")} width="17" height="22" />
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src={require("../../../res/img/cancel.svg")} width="18" height="18" <img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src={require("../../../res/img/cancel.svg")} width="18" height="18"
onClick={function() { ContentMessages.cancelUpload(upload.promise); }} onClick={function() { ContentMessages.sharedInstance().cancelUpload(upload.promise); }}
/> />
<div className="mx_UploadBar_uploadBytes"> <div className="mx_UploadBar_uploadBytes">
{ uploadedSize } / { totalSize } { uploadedSize } / { totalSize }

View file

@ -0,0 +1,106 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class UploadConfirmDialog extends React.Component {
static propTypes = {
file: PropTypes.object.isRequired,
currentIndex: PropTypes.number,
totalFiles: PropTypes.number,
onFinished: PropTypes.func.isRequired,
}
static defaultProps = {
totalFiles: 1,
}
constructor(props) {
super(props);
this._objectUrl = URL.createObjectURL(props.file);
}
componentWillUnmount() {
if (this._objectUrl) URL.revokeObjectURL(this._objectUrl);
}
_onCancelClick = () => {
this.props.onFinished(false);
}
_onUploadClick = () => {
this.props.onFinished(true);
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let title;
if (this.props.totalFiles > 1 && this.props.currentIndex !== undefined) {
title = _t(
"Upload files (%(current)s of %(total)s)",
{
current: this.props.currentIndex + 1,
total: this.props.totalFiles,
},
);
} else {
title = _t('Upload files');
}
let preview;
if (this.props.file.type.startsWith('image/')) {
preview = <div className="mx_UploadConfirmDialog_previewOuter">
<div className="mx_UploadConfirmDialog_previewInner">
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div>
<div>{this.props.file.name}</div>
</div>
</div>;
} else {
preview = <div>
<div>
<img className="mx_UploadConfirmDialog_fileIcon"
src={require("../../../../res/img/files.png")}
/>
{this.props.file.name}
</div>
</div>;
}
return (
<BaseDialog className='mx_UploadConfirmDialog'
onFinished={this._onCancelClick}
title={title}
contentId='mx_Dialog_content'
>
<div id='mx_Dialog_content'>
{preview}
</div>
<DialogButtons primaryButton={_t('Upload')}
hasCancel={false}
onPrimaryButtonClick={this._onUploadClick}
focus={true}
/>
</BaseDialog>
);
}
}

View file

@ -0,0 +1,120 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import filesize from 'filesize';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
/*
* Tells the user about files we know cannot be uploaded before we even try uploading
* them. This is named fairly generically but the only thing we check right now is
* the size of the file.
*/
export default class UploadFailureDialog extends React.Component {
static propTypes = {
badFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
totalFiles: PropTypes.number.isRequired,
contentMessages: PropTypes.instanceOf(ContentMessages).isRequired,
onFinished: PropTypes.func.isRequired,
}
_onCancelClick = () => {
this.props.onFinished(false);
}
_onUploadClick = () => {
this.props.onFinished(true);
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let message;
let preview;
let buttons;
if (this.props.totalFiles === 1 && this.props.badFiles.length === 1) {
message = _t(
"This file is <b>too large</b> to upload. " +
"The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.",
{
limit: filesize(this.props.contentMessages.getUploadLimit()),
sizeOfThisFile: filesize(this.props.badFiles[0].size),
}, {
b: sub => <b>{sub}</b>,
},
);
buttons = <DialogButtons primaryButton={_t('OK')}
hasCancel={false}
onPrimaryButtonClick={this._onCancelClick}
focus={true}
/>;
} else if (this.props.totalFiles === this.props.badFiles.length) {
message = _t(
"These files are <b>too large</b> to upload. " +
"The file size limit is %(limit)s.",
{
limit: filesize(this.props.contentMessages.getUploadLimit()),
}, {
b: sub => <b>{sub}</b>,
},
);
buttons = <DialogButtons primaryButton={_t('OK')}
hasCancel={false}
onPrimaryButtonClick={this._onCancelClick}
focus={true}
/>;
} else {
message = _t(
"Some files are <b>too large</b> to be uploaded. " +
"The file size limit is %(limit)s.",
{
limit: filesize(this.props.contentMessages.getUploadLimit()),
}, {
b: sub => <b>{sub}</b>,
},
);
const howManyOthers = this.props.totalFiles - this.props.badFiles.length;
buttons = <DialogButtons
primaryButton={_t('Upload %(count)s other files', { count: howManyOthers })}
onPrimaryButtonClick={this._onUploadClick}
hasCancel={true}
cancelButton={_t("Cancel All")}
onCancel={this._onCancelClick}
focus={true}
/>;
}
return (
<BaseDialog className='mx_UploadFailureDialog'
onFinished={this._onCancelClick}
title={_t("Upload Error")}
contentId='mx_Dialog_content'
>
<div id='mx_Dialog_content'>
{message}
{preview}
</div>
{buttons}
</BaseDialog>
);
}
}

View file

@ -26,6 +26,7 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker'; import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../matrix-to'; import { makeRoomPermalink } from '../../../matrix-to';
import ContentMessages from '../../../ContentMessages';
import classNames from 'classnames'; import classNames from 'classnames';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
@ -47,8 +48,7 @@ export default class MessageComposer extends React.Component {
this.onCallClick = this.onCallClick.bind(this); this.onCallClick = this.onCallClick.bind(this);
this.onHangupClick = this.onHangupClick.bind(this); this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.bind(this); this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileSelected = this.onUploadFileSelected.bind(this); this._onUploadFileInputChange = this._onUploadFileInputChange.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this); this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this); this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
@ -145,89 +145,25 @@ export default class MessageComposer extends React.Component {
this.refs.uploadInput.click(); this.refs.uploadInput.click();
} }
onUploadFileSelected(files) { _onUploadFileInputChange(ev) {
const tfiles = files.target.files; if (ev.target.files.length === 0) return;
this.uploadFiles(tfiles);
}
uploadFiles(files) { // take a copy so we can safely reset the value of the form control
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // (Note it is a FileList: we can't use slice or sesnible iteration).
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const tfiles = [];
for (let i = 0; i < ev.target.files.length; ++i) {
const fileList = []; tfiles.push(ev.target.files[i]);
const acceptedFiles = [];
const failedFiles = [];
for (let i=0; i<files.length; i++) {
const fileAcceptedOrError = this.props.uploadAllowed(files[i]);
if (fileAcceptedOrError === true) {
acceptedFiles.push(<li key={i}>
<TintableSvg key={i} src={require("../../../../res/img/files.svg")} width="16" height="16" /> { files[i].name || _t('Attachment') }
</li>);
fileList.push(files[i]);
} else {
failedFiles.push(<li key={i}>
<TintableSvg key={i} src={require("../../../../res/img/files.svg")} width="16" height="16" /> { files[i].name || _t('Attachment') } <p>{ _t('Reason') + ": " + fileAcceptedOrError}</p>
</li>);
}
} }
const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); ContentMessages.sharedInstance().sendContentListToRoom(
let replyToWarning = null; tfiles, this.props.room.roomId, MatrixClientPeg.get(),
if (isQuoting) {
replyToWarning = <p>{
_t('At this time it is not possible to reply with a file so this will be sent without being a reply.')
}</p>;
}
const acceptedFilesPart = acceptedFiles.length === 0 ? null : (
<div>
<p>{ _t('Are you sure you want to upload the following files?') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}>
{ acceptedFiles }
</ul>
</div>
); );
const failedFilesPart = failedFiles.length === 0 ? null : ( // This is the onChange handler for a file form control, but we're
<div> // not keeping any state, so reset the value of the form control
<p>{ _t('The following files cannot be uploaded:') }</p> // to empty.
<ul style={{listStyle: 'none', textAlign: 'left'}}> // NB. we need to set 'value': the 'files' property is immutable.
{ failedFiles } ev.target.value = '';
</ul>
</div>
);
let buttonText;
if (acceptedFiles.length > 0 && failedFiles.length > 0) {
buttonText = "Upload selected"
} else if (failedFiles.length > 0) {
buttonText = "Close"
}
Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
title: _t('Upload Files'),
description: (
<div>
{ acceptedFilesPart }
{ failedFilesPart }
{ replyToWarning }
</div>
),
hasCancelButton: acceptedFiles.length > 0,
button: buttonText,
onFinished: (shouldUpload) => {
if (shouldUpload) {
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
if (fileList) {
for (let i=0; i<fileList.length; i++) {
this.props.uploadFile(fileList[i]);
}
}
}
this.refs.uploadInput.value = null;
},
});
} }
onHangupClick() { onHangupClick() {
@ -376,7 +312,7 @@ export default class MessageComposer extends React.Component {
<input ref="uploadInput" type="file" <input ref="uploadInput" type="file"
style={uploadInputStyle} style={uploadInputStyle}
multiple multiple
onChange={this.onUploadFileSelected} /> onChange={this._onUploadFileInputChange} />
</AccessibleButton> </AccessibleButton>
); );
@ -414,7 +350,6 @@ export default class MessageComposer extends React.Component {
key="controls_input" key="controls_input"
room={this.props.room} room={this.props.room}
placeholder={placeholderText} placeholder={placeholderText}
onFilesPasted={this.uploadFiles}
onInputStateChanged={this.onInputStateChanged} onInputStateChanged={this.onInputStateChanged}
permalinkCreator={this.props.permalinkCreator} />, permalinkCreator={this.props.permalinkCreator} />,
formattingButton, formattingButton,
@ -510,12 +445,6 @@ MessageComposer.propTypes = {
// string representing the current voip call state // string representing the current voip call state
callState: PropTypes.string, callState: PropTypes.string,
// callback when a file to upload is chosen
uploadFile: PropTypes.func.isRequired,
// function to test whether a file should be allowed to be uploaded.
uploadAllowed: PropTypes.func.isRequired,
// string representing the current room app drawer state // string representing the current room app drawer state
showApps: PropTypes.bool showApps: PropTypes.bool
}; };

View file

@ -47,6 +47,7 @@ import {Completion} from "../../../autocomplete/Autocompleter";
import Markdown from '../../../Markdown'; import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager'; import ComposerHistoryManager from '../../../ComposerHistoryManager';
import MessageComposerStore from '../../../stores/MessageComposerStore'; import MessageComposerStore from '../../../stores/MessageComposerStore';
import ContentMessage from '../../../ContentMessages';
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix'; import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
@ -1009,7 +1010,13 @@ export default class MessageComposerInput extends React.Component {
switch (transfer.type) { switch (transfer.type) {
case 'files': case 'files':
return this.props.onFilesPasted(transfer.files); // This actually not so much for 'files' as such (at time of writing
// neither chrome nor firefox let you paste a plain file copied
// from Finder) but more images copied from a different website
// / word processor etc.
return ContentMessage.sharedInstance().sendContentListToRoom(
transfer.files, this.props.room.roomId, this.client,
);
case 'html': { case 'html': {
if (this.state.isRichTextEnabled) { if (this.state.isRichTextEnabled) {
// FIXME: https://github.com/ianstormtaylor/slate/issues/1497 means // FIXME: https://github.com/ianstormtaylor/slate/issues/1497 means

View file

@ -52,10 +52,15 @@ export default class RoomBreadcrumbs extends React.Component {
console.error("Failed to parse breadcrumbs:", e); console.error("Failed to parse breadcrumbs:", e);
} }
} }
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
} }
componentWillUnmount() { componentWillUnmount() {
dis.unregister(this._dispatcherRef); dis.unregister(this._dispatcherRef);
const client = MatrixClientPeg.get();
if (client) client.removeListener("Room.myMembership", this.onMyMembership);
} }
componentDidUpdate() { componentDidUpdate() {
@ -81,6 +86,17 @@ export default class RoomBreadcrumbs extends React.Component {
} }
} }
onMyMembership = (room, membership) => {
if (membership === "leave" || membership === "ban") {
const rooms = this.state.rooms.slice();
const roomState = rooms.find((r) => r.room.roomId === room.roomId);
if (roomState) {
roomState.left = true;
this.setState({rooms});
}
}
};
_appendRoomId(roomId) { _appendRoomId(roomId) {
const room = MatrixClientPeg.get().getRoom(roomId); const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) { if (!room) {
@ -130,23 +146,24 @@ export default class RoomBreadcrumbs extends React.Component {
return null; return null;
} }
const rooms = this.state.rooms; const rooms = this.state.rooms;
const avatars = rooms.map(({room, animated, hover}, i) => { const avatars = rooms.map((r, i) => {
const isFirst = i === 0; const isFirst = i === 0;
const classes = classNames({ const classes = classNames({
"mx_RoomBreadcrumbs_crumb": true, "mx_RoomBreadcrumbs_crumb": true,
"mx_RoomBreadcrumbs_preAnimate": isFirst && !animated, "mx_RoomBreadcrumbs_preAnimate": isFirst && !r.animated,
"mx_RoomBreadcrumbs_animate": isFirst, "mx_RoomBreadcrumbs_animate": isFirst,
"mx_RoomBreadcrumbs_left": r.left,
}); });
let tooltip = null; let tooltip = null;
if (hover) { if (r.hover) {
tooltip = <Tooltip label={room.name} />; tooltip = <Tooltip label={r.room.name} />;
} }
return ( return (
<AccessibleButton className={classes} key={room.roomId} onClick={() => this._viewRoom(room)} <AccessibleButton className={classes} key={r.room.roomId} onClick={() => this._viewRoom(r.room)}
onMouseEnter={() => this._onMouseEnter(room)} onMouseLeave={() => this._onMouseLeave(room)}> onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)}>
<RoomAvatar room={room} width={32} height={32} /> <RoomAvatar room={r.room} width={32} height={32} />
{tooltip} {tooltip}
</AccessibleButton> </AccessibleButton>
); );

View file

@ -29,9 +29,9 @@ import PersistedElement from "../elements/PersistedElement";
const widgetType = 'm.stickerpicker'; const widgetType = 'm.stickerpicker';
// We sit in a context menu, so the persisted element container needs to float // This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// above it, so it needs a greater z-index than the ContextMenu // We sit in a context menu, so this should be given to the context menu.
const STICKERPICKER_Z_INDEX = 5000; const STICKERPICKER_Z_INDEX = 3500;
// Key to store the widget's AppTile under in PersistedElement // Key to store the widget's AppTile under in PersistedElement
const PERSISTED_ELEMENT_KEY = "stickerPicker"; const PERSISTED_ELEMENT_KEY = "stickerPicker";
@ -373,6 +373,7 @@ export default class Stickerpicker extends React.Component {
menuPaddingTop={0} menuPaddingTop={0}
menuPaddingLeft={0} menuPaddingLeft={0}
menuPaddingRight={0} menuPaddingRight={0}
zIndex={STICKERPICKER_Z_INDEX}
/>; />;
if (this.state.showStickers) { if (this.state.showStickers) {

View file

@ -40,7 +40,10 @@
"A call is already in progress!": "A call is already in progress!", "A call is already in progress!": "A call is already in progress!",
"Permission Required": "Permission Required", "Permission Required": "Permission Required",
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
"The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload", "Replying With Files": "Replying With Files",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "At this time it is not possible to reply with a file. Would you like to upload this file without replying?",
"Continue": "Continue",
"The file '%(fileName)s' failed to upload.": "The file '%(fileName)s' failed to upload.",
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
"Upload Failed": "Upload Failed", "Upload Failed": "Upload Failed",
"Failure to create room": "Failure to create room", "Failure to create room": "Failure to create room",
@ -353,7 +356,6 @@
"Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.", "Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.",
"Unable to find a supported verification method.": "Unable to find a supported verification method.", "Unable to find a supported verification method.": "Unable to find a supported verification method.",
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "For maximum security, we recommend you do this in person or use another trusted means of communication.", "For maximum security, we recommend you do this in person or use another trusted means of communication.": "For maximum security, we recommend you do this in person or use another trusted means of communication.",
"Continue": "Continue",
"Dog": "Dog", "Dog": "Dog",
"Cat": "Cat", "Cat": "Cat",
"Lion": "Lion", "Lion": "Lion",
@ -718,11 +720,6 @@
"block-quote": "block-quote", "block-quote": "block-quote",
"bulleted-list": "bulleted-list", "bulleted-list": "bulleted-list",
"numbered-list": "numbered-list", "numbered-list": "numbered-list",
"Attachment": "Attachment",
"At this time it is not possible to reply with a file so this will be sent without being a reply.": "At this time it is not possible to reply with a file so this will be sent without being a reply.",
"Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?",
"The following files cannot be uploaded:": "The following files cannot be uploaded:",
"Upload Files": "Upload Files",
"Hangup": "Hangup", "Hangup": "Hangup",
"Voice call": "Voice call", "Voice call": "Voice call",
"Video call": "Video call", "Video call": "Video call",
@ -873,6 +870,7 @@
"Today": "Today", "Today": "Today",
"Yesterday": "Yesterday", "Yesterday": "Yesterday",
"Error decrypting audio": "Error decrypting audio", "Error decrypting audio": "Error decrypting audio",
"Attachment": "Attachment",
"Error decrypting attachment": "Error decrypting attachment", "Error decrypting attachment": "Error decrypting attachment",
"Decrypt %(text)s": "Decrypt %(text)s", "Decrypt %(text)s": "Decrypt %(text)s",
"Download %(text)s": "Download %(text)s", "Download %(text)s": "Download %(text)s",
@ -1196,6 +1194,16 @@
"Room contains unknown devices": "Room contains unknown devices", "Room contains unknown devices": "Room contains unknown devices",
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
"Unknown devices": "Unknown devices", "Unknown devices": "Unknown devices",
"Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)",
"Upload files": "Upload files",
"Upload": "Upload",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.",
"These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "These files are <b>too large</b> to upload. The file size limit is %(limit)s.",
"Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.",
"Upload %(count)s other files|other": "Upload %(count)s other files",
"Upload %(count)s other files|one": "Upload %(count)s other file",
"Cancel All": "Cancel All",
"Upload Error": "Upload Error",
"A widget would like to verify your identity": "A widget would like to verify your identity", "A widget would like to verify your identity": "A widget would like to verify your identity",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.",
"Remember my selection for this widget": "Remember my selection for this widget", "Remember my selection for this widget": "Remember my selection for this widget",
@ -1421,9 +1429,6 @@
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?", "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
"File is too big. Maximum file size is %(fileSize)s": "File is too big. Maximum file size is %(fileSize)s",
"Failed to upload file": "Failed to upload file",
"Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big",
"Search failed": "Search failed", "Search failed": "Search failed",
"Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(", "Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(",
"No more results": "No more results", "No more results": "No more results",

View file

@ -4379,7 +4379,7 @@ math-random@^1.0.1:
resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
matrix-js-sdk@^1.0.3: matrix-js-sdk@1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-1.0.3.tgz#d4cc46c4dc80278b78f8e0664741b08fcc395c79" resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-1.0.3.tgz#d4cc46c4dc80278b78f8e0664741b08fcc395c79"
integrity sha512-YpF4NvnG2cttRmTPJ9yqs/KwlBXW15O7+nNMs1FKj1CqdW1Phwb0fcqvahjPgmfXyn5DFzU3Deiv9aNgDIlIog== integrity sha512-YpF4NvnG2cttRmTPJ9yqs/KwlBXW15O7+nNMs1FKj1CqdW1Phwb0fcqvahjPgmfXyn5DFzU3Deiv9aNgDIlIog==