mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 21:24:59 +08:00
Merge branch 'develop' into travis/stickerpicker/remount
This commit is contained in:
commit
ad777782b8
@ -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
|
||||||
|
@ -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";
|
||||||
|
33
res/css/views/dialogs/_UploadConfirmDialog.scss
Normal file
33
res/css/views/dialogs/_UploadConfirmDialog.scss
Normal 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%;
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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;
|
|
||||||
|
@ -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);
|
||||||
|
@ -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>;
|
||||||
|
@ -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}
|
||||||
/>;
|
/>;
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
|
106
src/components/views/dialogs/UploadConfirmDialog.js
Normal file
106
src/components/views/dialogs/UploadConfirmDialog.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
120
src/components/views/dialogs/UploadFailureDialog.js
Normal file
120
src/components/views/dialogs/UploadFailureDialog.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
|
// take a copy so we can safely reset the value of the form control
|
||||||
|
// (Note it is a FileList: we can't use slice or sesnible iteration).
|
||||||
|
const tfiles = [];
|
||||||
|
for (let i = 0; i < ev.target.files.length; ++i) {
|
||||||
|
tfiles.push(ev.target.files[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFiles(files) {
|
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
tfiles, this.props.room.roomId, MatrixClientPeg.get(),
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
|
||||||
|
|
||||||
const fileList = [];
|
|
||||||
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());
|
|
||||||
let replyToWarning = null;
|
|
||||||
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
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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) {
|
||||||
|
@ -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",
|
||||||
|
@ -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==
|
||||||
|
Loading…
Reference in New Issue
Block a user