Merge branch 'develop' into luke/store-history-as-raw-content
This commit is contained in:
commit
6b1b643d41
82 changed files with 1226 additions and 495 deletions
|
@ -4,7 +4,7 @@ set -e
|
||||||
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||||
nvm use 4
|
nvm use 6
|
||||||
|
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,18 @@ module.exports = function (config) {
|
||||||
// test results reporter to use
|
// test results reporter to use
|
||||||
// possible values: 'dots', 'progress'
|
// possible values: 'dots', 'progress'
|
||||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||||
reporters: ['progress', 'junit'],
|
reporters: ['logcapture', 'spec', 'junit', 'summary'],
|
||||||
|
|
||||||
|
specReporter: {
|
||||||
|
suppressErrorSummary: false, // do print error summary
|
||||||
|
suppressFailed: false, // do print information about failed tests
|
||||||
|
suppressPassed: false, // do print information about passed tests
|
||||||
|
showSpecTiming: true, // print the time elapsed for each spec
|
||||||
|
},
|
||||||
|
|
||||||
|
client: {
|
||||||
|
captureLogs: true,
|
||||||
|
},
|
||||||
|
|
||||||
// web server port
|
// web server port
|
||||||
port: 9876,
|
port: 9876,
|
||||||
|
@ -104,7 +115,10 @@ module.exports = function (config) {
|
||||||
// level of logging
|
// level of logging
|
||||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR ||
|
// possible values: config.LOG_DISABLE || config.LOG_ERROR ||
|
||||||
// config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
// config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||||
logLevel: config.LOG_INFO,
|
//
|
||||||
|
// This is strictly for logs that would be generated by the browser itself and we
|
||||||
|
// don't want to log about missing images, which are emitted on LOG_WARN.
|
||||||
|
logLevel: config.LOG_ERROR,
|
||||||
|
|
||||||
// enable / disable watching file and executing tests whenever any file
|
// enable / disable watching file and executing tests whenever any file
|
||||||
// changes
|
// changes
|
||||||
|
|
11
package.json
11
package.json
|
@ -53,9 +53,9 @@
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
"commonmark": "^0.27.0",
|
"commonmark": "^0.27.0",
|
||||||
"counterpart": "^0.18.0",
|
"counterpart": "^0.18.0",
|
||||||
"draft-js": "^0.9.1",
|
"draft-js": "^0.11.0-alpha",
|
||||||
"draft-js-export-html": "^0.5.0",
|
"draft-js-export-html": "^0.6.0",
|
||||||
"draft-js-export-markdown": "^0.2.0",
|
"draft-js-export-markdown": "^0.3.0",
|
||||||
"emojione": "2.2.7",
|
"emojione": "2.2.7",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "3.5.6",
|
"filesize": "3.5.6",
|
||||||
|
@ -102,12 +102,15 @@
|
||||||
"eslint-plugin-react": "^6.9.0",
|
"eslint-plugin-react": "^6.9.0",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
"karma": "^0.13.22",
|
"karma": "^1.7.0",
|
||||||
"karma-chrome-launcher": "^0.2.3",
|
"karma-chrome-launcher": "^0.2.3",
|
||||||
"karma-cli": "^0.1.2",
|
"karma-cli": "^0.1.2",
|
||||||
"karma-junit-reporter": "^0.4.1",
|
"karma-junit-reporter": "^0.4.1",
|
||||||
|
"karma-logcapture-reporter": "0.0.1",
|
||||||
"karma-mocha": "^0.2.2",
|
"karma-mocha": "^0.2.2",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
|
"karma-spec-reporter": "^0.0.31",
|
||||||
|
"karma-summary-reporter": "^1.3.3",
|
||||||
"karma-webpack": "^1.7.0",
|
"karma-webpack": "^1.7.0",
|
||||||
"matrix-react-test-utils": "^0.1.1",
|
"matrix-react-test-utils": "^0.1.1",
|
||||||
"mocha": "^2.4.5",
|
"mocha": "^2.4.5",
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCurrentLanguage } from './languageHandler';
|
import { getCurrentLanguage } from './languageHandler';
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
|
|
||||||
|
@ -31,8 +30,18 @@ const customVariables = {
|
||||||
'User Type': 3,
|
'User Type': 3,
|
||||||
'Chosen Language': 4,
|
'Chosen Language': 4,
|
||||||
'Instance': 5,
|
'Instance': 5,
|
||||||
|
'RTE: Uses Richtext Mode': 6,
|
||||||
|
'Homeserver URL': 7,
|
||||||
|
'Identity Server URL': 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function whitelistRedact(whitelist, str) {
|
||||||
|
if (whitelist.includes(str)) return str;
|
||||||
|
return '<redacted>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const whitelistedHSUrls = ["https://matrix.org"];
|
||||||
|
const whitelistedISUrls = ["https://vector.im"];
|
||||||
|
|
||||||
class Analytics {
|
class Analytics {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -76,7 +85,7 @@ class Analytics {
|
||||||
this._paq.push(['trackAllContentImpressions']);
|
this._paq.push(['trackAllContentImpressions']);
|
||||||
this._paq.push(['discardHashTag', false]);
|
this._paq.push(['discardHashTag', false]);
|
||||||
this._paq.push(['enableHeartBeatTimer']);
|
this._paq.push(['enableHeartBeatTimer']);
|
||||||
this._paq.push(['enableLinkTracking', true]);
|
// this._paq.push(['enableLinkTracking', true]);
|
||||||
|
|
||||||
const platform = PlatformPeg.get();
|
const platform = PlatformPeg.get();
|
||||||
this._setVisitVariable('App Platform', platform.getHumanReadableName());
|
this._setVisitVariable('App Platform', platform.getHumanReadableName());
|
||||||
|
@ -130,20 +139,20 @@ class Analytics {
|
||||||
this._paq.push(['deleteCookies']);
|
this._paq.push(['deleteCookies']);
|
||||||
}
|
}
|
||||||
|
|
||||||
login() { // not used currently
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
if (this.disabled || !cli) return;
|
|
||||||
|
|
||||||
this._paq.push(['setUserId', `@${cli.getUserIdLocalpart()}:${cli.getDomain()}`]);
|
|
||||||
}
|
|
||||||
|
|
||||||
_setVisitVariable(key, value) {
|
_setVisitVariable(key, value) {
|
||||||
this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
|
this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
|
||||||
}
|
}
|
||||||
|
|
||||||
setGuest(guest) {
|
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In');
|
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||||
|
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||||
|
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
setRichtextMode(state) {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -143,7 +143,7 @@ function _setCallListeners(call) {
|
||||||
pause("ringbackAudio");
|
pause("ringbackAudio");
|
||||||
play("busyAudio");
|
play("busyAudio");
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
||||||
title: _t('Call Timeout'),
|
title: _t('Call Timeout'),
|
||||||
description: _t('The remote side failed to pick up') + '.',
|
description: _t('The remote side failed to pick up') + '.',
|
||||||
});
|
});
|
||||||
|
@ -205,7 +205,7 @@ function _onAction(payload) {
|
||||||
_setCallState(undefined, newCall.roomId, "ended");
|
_setCallState(undefined, newCall.roomId, "ended");
|
||||||
console.log("Can't capture screen: " + screenCapErrorString);
|
console.log("Can't capture screen: " + screenCapErrorString);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
|
||||||
title: _t('Unable to capture screen'),
|
title: _t('Unable to capture screen'),
|
||||||
description: screenCapErrorString,
|
description: screenCapErrorString,
|
||||||
});
|
});
|
||||||
|
@ -225,7 +225,7 @@ function _onAction(payload) {
|
||||||
case 'place_call':
|
case 'place_call':
|
||||||
if (module.exports.getAnyActiveCall()) {
|
if (module.exports.getAnyActiveCall()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||||
title: _t('Existing Call'),
|
title: _t('Existing Call'),
|
||||||
description: _t('You are already in a call.'),
|
description: _t('You are already in a call.'),
|
||||||
});
|
});
|
||||||
|
@ -235,7 +235,7 @@ function _onAction(payload) {
|
||||||
// if the runtime env doesn't do VoIP, whine.
|
// if the runtime env doesn't do VoIP, whine.
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||||
title: _t('VoIP is unsupported'),
|
title: _t('VoIP is unsupported'),
|
||||||
description: _t('You cannot place VoIP calls in this browser.'),
|
description: _t('You cannot place VoIP calls in this browser.'),
|
||||||
});
|
});
|
||||||
|
@ -251,7 +251,7 @@ function _onAction(payload) {
|
||||||
var members = room.getJoinedMembers();
|
var members = room.getJoinedMembers();
|
||||||
if (members.length <= 1) {
|
if (members.length <= 1) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
||||||
description: _t('You cannot place a call with yourself.'),
|
description: _t('You cannot place a call with yourself.'),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -277,13 +277,13 @@ function _onAction(payload) {
|
||||||
console.log("Place conference call in %s", payload.room_id);
|
console.log("Place conference call in %s", payload.room_id);
|
||||||
if (!ConferenceHandler) {
|
if (!ConferenceHandler) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
||||||
description: _t('Conference calls are not supported in this client'),
|
description: _t('Conference calls are not supported in this client'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (!MatrixClientPeg.get().supportsVoip()) {
|
else if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||||
title: _t('VoIP is unsupported'),
|
title: _t('VoIP is unsupported'),
|
||||||
description: _t('You cannot place VoIP calls in this browser.'),
|
description: _t('You cannot place VoIP calls in this browser.'),
|
||||||
});
|
});
|
||||||
|
@ -296,13 +296,13 @@ function _onAction(payload) {
|
||||||
// participant.
|
// participant.
|
||||||
// Therefore we disable conference calling in E2E rooms.
|
// Therefore we disable conference calling in E2E rooms.
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
||||||
description: _t('Conference calls are not supported in encrypted rooms'),
|
description: _t('Conference calls are not supported in encrypted rooms'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
||||||
title: _t('Warning!'),
|
title: _t('Warning!'),
|
||||||
description: _t('Conference calling is in development and may not be reliable.'),
|
description: _t('Conference calling is in development and may not be reliable.'),
|
||||||
onFinished: confirm=>{
|
onFinished: confirm=>{
|
||||||
|
@ -314,7 +314,7 @@ function _onAction(payload) {
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Conference call failed: " + err);
|
console.error("Conference call failed: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, {
|
||||||
title: _t('Failed to set up conference call'),
|
title: _t('Failed to set up conference call'),
|
||||||
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
|
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
|
||||||
});
|
});
|
||||||
|
|
|
@ -360,7 +360,7 @@ class ContentMessages {
|
||||||
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
||||||
}
|
}
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||||
title: _t('Upload Failed'),
|
title: _t('Upload Failed'),
|
||||||
description: desc,
|
description: desc,
|
||||||
});
|
});
|
||||||
|
|
|
@ -145,7 +145,7 @@ const sanitizeHtmlParams = {
|
||||||
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||||
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||||
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||||
img: ['src'],
|
img: ['src', 'width', 'height', 'alt', 'title'],
|
||||||
ol: ['start'],
|
ol: ['start'],
|
||||||
code: ['class'], // We don't actually allow all classes, we filter them in transformTags
|
code: ['class'], // We don't actually allow all classes, we filter them in transformTags
|
||||||
},
|
},
|
||||||
|
|
|
@ -125,7 +125,7 @@ export default class KeyRequestHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
|
const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
|
||||||
Modal.createDialog(KeyShareDialog, {
|
Modal.createTrackedDialog('Key Share', 'Process Next Request', KeyShareDialog, {
|
||||||
matrixClient: this._matrixClient,
|
matrixClient: this._matrixClient,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
|
|
|
@ -240,7 +240,7 @@ function _handleRestoreFailure(e) {
|
||||||
const SessionRestoreErrorDialog =
|
const SessionRestoreErrorDialog =
|
||||||
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
|
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
|
||||||
|
|
||||||
Modal.createDialog(SessionRestoreErrorDialog, {
|
Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, {
|
||||||
error: e.message,
|
error: e.message,
|
||||||
onFinished: (success) => {
|
onFinished: (success) => {
|
||||||
def.resolve(success);
|
def.resolve(success);
|
||||||
|
@ -318,7 +318,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||||
await _clearStorage();
|
await _clearStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
Analytics.setGuest(credentials.guest);
|
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl);
|
||||||
|
|
||||||
// Resolves by default
|
// Resolves by default
|
||||||
let teamPromise = Promise.resolve(null);
|
let teamPromise = Promise.resolve(null);
|
||||||
|
|
|
@ -55,6 +55,25 @@ function is_multi_line(node) {
|
||||||
return par.firstChild != par.lastChild;
|
return par.firstChild != par.lastChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import linkifyMatrix from './linkify-matrix';
|
||||||
|
import * as linkify from 'linkifyjs';
|
||||||
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
// Thieved from draft-js-export-markdown
|
||||||
|
function escapeMarkdown(s) {
|
||||||
|
return s.replace(/[*_`]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace URLs, room aliases and user IDs with md-escaped URLs
|
||||||
|
function linkifyMarkdown(s) {
|
||||||
|
const links = linkify.find(s);
|
||||||
|
links.forEach((l) => {
|
||||||
|
// This may replace several instances of `l.value` at once, but that's OK
|
||||||
|
s = s.replace(l.value, escapeMarkdown(l.value));
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that wraps commonmark, adding the ability to see whether
|
* Class that wraps commonmark, adding the ability to see whether
|
||||||
* a given message actually uses any markdown syntax or whether
|
* a given message actually uses any markdown syntax or whether
|
||||||
|
@ -62,7 +81,7 @@ function is_multi_line(node) {
|
||||||
*/
|
*/
|
||||||
export default class Markdown {
|
export default class Markdown {
|
||||||
constructor(input) {
|
constructor(input) {
|
||||||
this.input = input;
|
this.input = linkifyMarkdown(input);
|
||||||
|
|
||||||
const parser = new commonmark.Parser();
|
const parser = new commonmark.Parser();
|
||||||
this.parsed = parser.parse(this.input);
|
this.parsed = parser.parse(this.input);
|
||||||
|
|
13
src/Modal.js
13
src/Modal.js
|
@ -103,13 +103,20 @@ class ModalManager {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createTrackedDialog(analyticsAction, analyticsInfo, Element, props, className) {
|
||||||
|
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
||||||
|
return this.createDialog(Element, props, className);
|
||||||
|
}
|
||||||
|
|
||||||
createDialog(Element, props, className) {
|
createDialog(Element, props, className) {
|
||||||
if (props && props.title) {
|
|
||||||
Analytics.trackEvent('Modal', props.title, 'createDialog');
|
|
||||||
}
|
|
||||||
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
|
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createTrackedDialogAsync(analyticsId, loader, props, className) {
|
||||||
|
Analytics.trackEvent('Modal', analyticsId);
|
||||||
|
return this.createDialogAsync(loader, props, className);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a modal view.
|
* Open a modal view.
|
||||||
*
|
*
|
||||||
|
|
|
@ -142,7 +142,7 @@ const Notifier = {
|
||||||
? _t('Riot does not have permission to send you notifications - please check your browser settings')
|
? _t('Riot does not have permission to send you notifications - please check your browser settings')
|
||||||
: _t('Riot was not given permission to send notifications - please try again');
|
: _t('Riot was not given permission to send notifications - please try again');
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, {
|
||||||
title: _t('Unable to enable Notifications'),
|
title: _t('Unable to enable Notifications'),
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,7 +51,8 @@ export const contentStateToHTML = (contentState: ContentState) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function htmlToContentState(html: string): ContentState {
|
export function htmlToContentState(html: string): ContentState {
|
||||||
return ContentState.createFromBlockArray(convertFromHTML(html));
|
const blockArray = convertFromHTML(html).contentBlocks;
|
||||||
|
return ContentState.createFromBlockArray(blockArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unicodeToEmojiUri(str) {
|
function unicodeToEmojiUri(str) {
|
||||||
|
@ -90,7 +91,7 @@ function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: numb
|
||||||
|
|
||||||
// Workaround for https://github.com/facebook/draft-js/issues/414
|
// Workaround for https://github.com/facebook/draft-js/issues/414
|
||||||
let emojiDecorator = {
|
let emojiDecorator = {
|
||||||
strategy: (contentBlock, callback) => {
|
strategy: (contentState, contentBlock, callback) => {
|
||||||
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
||||||
},
|
},
|
||||||
component: (props) => {
|
component: (props) => {
|
||||||
|
@ -119,7 +120,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
||||||
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
|
let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
|
||||||
(style) => ({
|
(style) => ({
|
||||||
strategy: (contentBlock, callback) => {
|
strategy: (contentState, contentBlock, callback) => {
|
||||||
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
|
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
|
||||||
},
|
},
|
||||||
component: (props) => (
|
component: (props) => (
|
||||||
|
@ -130,7 +131,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
markdownDecorators.push({
|
markdownDecorators.push({
|
||||||
strategy: (contentBlock, callback) => {
|
strategy: (contentState, contentBlock, callback) => {
|
||||||
return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback);
|
return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback);
|
||||||
},
|
},
|
||||||
component: (props) => (
|
component: (props) => (
|
||||||
|
@ -201,10 +202,8 @@ export function selectionStateToTextOffsets(selectionState: SelectionState,
|
||||||
export function textOffsetsToSelectionState({start, end}: SelectionRange,
|
export function textOffsetsToSelectionState({start, end}: SelectionRange,
|
||||||
contentBlocks: Array<ContentBlock>): SelectionState {
|
contentBlocks: Array<ContentBlock>): SelectionState {
|
||||||
let selectionState = SelectionState.createEmpty();
|
let selectionState = SelectionState.createEmpty();
|
||||||
|
for (const block of contentBlocks) {
|
||||||
for (let block of contentBlocks) {
|
const blockLength = block.getLength();
|
||||||
let blockLength = block.getLength();
|
|
||||||
|
|
||||||
if (start !== -1 && start < blockLength) {
|
if (start !== -1 && start < blockLength) {
|
||||||
selectionState = selectionState.merge({
|
selectionState = selectionState.merge({
|
||||||
anchorKey: block.getKey(),
|
anchorKey: block.getKey(),
|
||||||
|
@ -212,9 +211,8 @@ export function textOffsetsToSelectionState({start, end}: SelectionRange,
|
||||||
});
|
});
|
||||||
start = -1;
|
start = -1;
|
||||||
} else {
|
} else {
|
||||||
start -= blockLength;
|
start -= blockLength + 1; // +1 to account for newline between blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end !== -1 && end <= blockLength) {
|
if (end !== -1 && end <= blockLength) {
|
||||||
selectionState = selectionState.merge({
|
selectionState = selectionState.merge({
|
||||||
focusKey: block.getKey(),
|
focusKey: block.getKey(),
|
||||||
|
@ -222,10 +220,9 @@ export function textOffsetsToSelectionState({start, end}: SelectionRange,
|
||||||
});
|
});
|
||||||
end = -1;
|
end = -1;
|
||||||
} else {
|
} else {
|
||||||
end -= blockLength;
|
end -= blockLength + 1; // +1 to account for newline between blocks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectionState;
|
return selectionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +239,7 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
|
||||||
const existingEntityKey = block.getEntityAt(start);
|
const existingEntityKey = block.getEntityAt(start);
|
||||||
if (existingEntityKey) {
|
if (existingEntityKey) {
|
||||||
// avoid manipulation in case the emoji already has an entity
|
// avoid manipulation in case the emoji already has an entity
|
||||||
const entity = Entity.get(existingEntityKey);
|
const entity = newContentState.getEntity(existingEntityKey);
|
||||||
if (entity && entity.get('type') === 'emoji') {
|
if (entity && entity.get('type') === 'emoji') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -252,7 +249,10 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
|
||||||
.set('anchorOffset', start)
|
.set('anchorOffset', start)
|
||||||
.set('focusOffset', end);
|
.set('focusOffset', end);
|
||||||
const emojiText = plainText.substring(start, end);
|
const emojiText = plainText.substring(start, end);
|
||||||
const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText });
|
newContentState = newContentState.createEntity(
|
||||||
|
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText }
|
||||||
|
);
|
||||||
|
const entityKey = newContentState.getLastCreatedEntityKey();
|
||||||
newContentState = Modifier.replaceText(
|
newContentState = Modifier.replaceText(
|
||||||
newContentState,
|
newContentState,
|
||||||
selection,
|
selection,
|
||||||
|
|
|
@ -68,7 +68,7 @@ const commands = {
|
||||||
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
// TODO Don't explain this away, actually show a search UI here.
|
// TODO Don't explain this away, actually show a search UI here.
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
||||||
title: _t('/ddg is not a command'),
|
title: _t('/ddg is not a command'),
|
||||||
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
||||||
});
|
});
|
||||||
|
@ -326,13 +326,11 @@ const commands = {
|
||||||
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
|
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return MatrixClientPeg.get().setDeviceVerified(
|
return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true);
|
||||||
userId, deviceId, true,
|
|
||||||
);
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// Tell the user we verified everything
|
// Tell the user we verified everything
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
||||||
title: _t("Verified key"),
|
title: _t("Verified key"),
|
||||||
description: (
|
description: (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const onAction = function(payload) {
|
||||||
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
|
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
|
||||||
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
Modal.createDialog(UnknownDeviceDialog, {
|
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
|
||||||
devices: payload.err.devices,
|
devices: payload.err.devices,
|
||||||
room: payload.room,
|
room: payload.room,
|
||||||
onFinished: (r) => {
|
onFinished: (r) => {
|
||||||
|
|
|
@ -15,6 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
import UserSettingsStore from './UserSettingsStore';
|
||||||
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
var sdk = require('./index');
|
var sdk = require('./index');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -63,6 +65,7 @@ module.exports = {
|
||||||
// we have and the read receipt. We could fetch more history to try & find out,
|
// we have and the read receipt. We could fetch more history to try & find out,
|
||||||
// but currently we just guess.
|
// but currently we just guess.
|
||||||
|
|
||||||
|
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
// Loop through messages, starting with the most recent...
|
// Loop through messages, starting with the most recent...
|
||||||
for (var i = room.timeline.length - 1; i >= 0; --i) {
|
for (var i = room.timeline.length - 1; i >= 0; --i) {
|
||||||
var ev = room.timeline[i];
|
var ev = room.timeline[i];
|
||||||
|
@ -72,7 +75,7 @@ module.exports = {
|
||||||
// that counts and we can stop looking because the user's read
|
// that counts and we can stop looking because the user's read
|
||||||
// this and everything before.
|
// this and everything before.
|
||||||
return false;
|
return false;
|
||||||
} else if (this.eventTriggersUnreadCount(ev)) {
|
} else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) {
|
||||||
// We've found a message that counts before we hit
|
// We've found a message that counts before we hit
|
||||||
// the read marker, so this room is definitely unread.
|
// the read marker, so this room is definitely unread.
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -29,6 +29,9 @@ export default {
|
||||||
name: "-",
|
name: "-",
|
||||||
id: 'matrix_apps',
|
id: 'matrix_apps',
|
||||||
default: false,
|
default: false,
|
||||||
|
|
||||||
|
// XXX: Always use default, ignore localStorage and remove from labs
|
||||||
|
override: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -171,22 +174,36 @@ export default {
|
||||||
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
||||||
},
|
},
|
||||||
|
|
||||||
isFeatureEnabled: function(feature: string): boolean {
|
getFeatureById(feature: string) {
|
||||||
|
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
|
||||||
|
const f = this.LABS_FEATURES[i];
|
||||||
|
if (f.id === feature) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
isFeatureEnabled: function(featureId: string): boolean {
|
||||||
// Disable labs for guests.
|
// Disable labs for guests.
|
||||||
if (MatrixClientPeg.get().isGuest()) return false;
|
if (MatrixClientPeg.get().isGuest()) return false;
|
||||||
|
|
||||||
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
|
const feature = this.getFeatureById(featureId);
|
||||||
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
|
if (!feature) {
|
||||||
const f = this.LABS_FEATURES[i];
|
console.warn(`Unknown feature "${featureId}"`);
|
||||||
if (f.id === feature) {
|
return false;
|
||||||
return f.default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return localStorage.getItem(`mx_labs_feature_${feature}`) === 'true';
|
// Return the default if this feature has an override to be the default value or
|
||||||
|
// if the feature has never been toggled and is therefore not in localStorage
|
||||||
|
if (Object.keys(feature).includes('override') ||
|
||||||
|
localStorage.getItem(`mx_labs_feature_${featureId}`) === null
|
||||||
|
) {
|
||||||
|
return feature.default;
|
||||||
|
}
|
||||||
|
return localStorage.getItem(`mx_labs_feature_${featureId}`) === 'true';
|
||||||
},
|
},
|
||||||
|
|
||||||
setFeatureEnabled: function(feature: string, enabled: boolean) {
|
setFeatureEnabled: function(featureId: string, enabled: boolean) {
|
||||||
localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
|
localStorage.setItem(`mx_labs_feature_${featureId}`, enabled);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
58
src/WidgetUtils.js
Normal file
58
src/WidgetUtils.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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 MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
|
export default class WidgetUtils {
|
||||||
|
|
||||||
|
/* Returns true if user is able to send state events to modify widgets in this room
|
||||||
|
* @param roomId -- The ID of the room to check
|
||||||
|
* @return Boolean -- true if the user can modify widgets in this room
|
||||||
|
* @throws Error -- specifies the error reason
|
||||||
|
*/
|
||||||
|
static canUserModifyWidgets(roomId) {
|
||||||
|
if (!roomId) {
|
||||||
|
console.warn('No room ID specified');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
console.warn('User must be be logged in');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
console.warn(`Room ID ${roomId} is not recognised`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const me = client.credentials.userId;
|
||||||
|
if (!me) {
|
||||||
|
console.warn('Failed to get user ID');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = room.getMember(me);
|
||||||
|
if (!member || member.membership !== "join") {
|
||||||
|
console.warn(`User ${me} is not in room ${roomId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return room.currentState.maySendStateEvent('im.vector.modular.widgets', me);
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,6 +71,15 @@ const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sor
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
|
function score(query, space) {
|
||||||
|
const index = space.indexOf(query);
|
||||||
|
if (index === -1) {
|
||||||
|
return Infinity;
|
||||||
|
} else {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class EmojiProvider extends AutocompleteProvider {
|
export default class EmojiProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EMOJI_REGEX);
|
super(EMOJI_REGEX);
|
||||||
|
@ -104,8 +113,20 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
||||||
completions = completions.concat(this.nameMatcher.match(matchedString));
|
completions = completions.concat(this.nameMatcher.match(matchedString));
|
||||||
// Reinstate original order
|
|
||||||
completions = _sortBy(_uniq(completions), '_orderBy');
|
const sorters = [];
|
||||||
|
// First, sort by score (Infinity if matchedString not in shortname)
|
||||||
|
sorters.push((c) => score(matchedString, c.shortname));
|
||||||
|
// If the matchedString is not empty, sort by length of shortname. Example:
|
||||||
|
// matchedString = ":bookmark"
|
||||||
|
// completions = [":bookmark:", ":bookmark_tabs:", ...]
|
||||||
|
if (matchedString.length > 1) {
|
||||||
|
sorters.push((c) => c.shortname.length);
|
||||||
|
}
|
||||||
|
// Finally, sort by original ordering
|
||||||
|
sorters.push((c) => c._orderBy);
|
||||||
|
completions = _sortBy(_uniq(completions), sorters);
|
||||||
|
|
||||||
completions = completions.map((result) => {
|
completions = completions.map((result) => {
|
||||||
const {shortname} = result;
|
const {shortname} = result;
|
||||||
const unicode = shortnameToUnicode(shortname);
|
const unicode = shortnameToUnicode(shortname);
|
||||||
|
|
|
@ -23,35 +23,58 @@ import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import {getDisplayAliasForRoom} from '../Rooms';
|
import {getDisplayAliasForRoom} from '../Rooms';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
|
||||||
const ROOM_REGEX = /(?=#)(\S*)/g;
|
const ROOM_REGEX = /(?=#)(\S*)/g;
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
|
function score(query, space) {
|
||||||
|
const index = space.indexOf(query);
|
||||||
|
if (index === -1) {
|
||||||
|
return Infinity;
|
||||||
|
} else {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class RoomProvider extends AutocompleteProvider {
|
export default class RoomProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ROOM_REGEX);
|
super(ROOM_REGEX);
|
||||||
this.matcher = new FuzzyMatcher([], {
|
this.matcher = new FuzzyMatcher([], {
|
||||||
keys: ['name', 'roomId', 'aliases'],
|
keys: ['displayedAlias', 'name'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
let client = MatrixClientPeg.get();
|
// Disable autocompletions when composing commands because of various issues
|
||||||
|
// (see https://github.com/vector-im/riot-web/issues/4762)
|
||||||
|
if (/^(\/join|\/leave)/.test(query)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
// the only reason we need to do this is because Fuse only matches on properties
|
// the only reason we need to do this is because Fuse only matches on properties
|
||||||
this.matcher.setObjects(client.getRooms().filter(room => !!room && !!getDisplayAliasForRoom(room)).map(room => {
|
this.matcher.setObjects(client.getRooms().filter(
|
||||||
|
(room) => !!room && !!getDisplayAliasForRoom(room),
|
||||||
|
).map((room) => {
|
||||||
return {
|
return {
|
||||||
room: room,
|
room: room,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
aliases: room.getAliases(),
|
displayedAlias: getDisplayAliasForRoom(room),
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
completions = this.matcher.match(command[0]).map(room => {
|
const matchedString = command[0];
|
||||||
|
completions = this.matcher.match(matchedString);
|
||||||
|
completions = _sortBy(completions, [
|
||||||
|
(c) => score(matchedString, c.displayedAlias),
|
||||||
|
(c) => c.displayedAlias.length,
|
||||||
|
]).map((room) => {
|
||||||
const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
||||||
return {
|
return {
|
||||||
completion: displayAlias,
|
completion: displayAlias,
|
||||||
|
@ -62,7 +85,9 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).filter(completion => !!completion.completion && completion.completion.length > 0).slice(0, 4);
|
})
|
||||||
|
.filter((completion) => !!completion.completion && completion.completion.length > 0)
|
||||||
|
.slice(0, 4);
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,13 +48,21 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||||
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
||||||
|
|
||||||
|
// Disable autocompletions when composing commands because of various issues
|
||||||
|
// (see https://github.com/vector-im/riot-web/issues/4762)
|
||||||
|
if (/^(\/ban|\/unban|\/op|\/deop|\/invite|\/kick|\/verify)/.test(query)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.matcher.match(command[0]).map((user) => {
|
completions = this.matcher.match(command[0]).map((user) => {
|
||||||
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||||
return {
|
return {
|
||||||
completion: displayName,
|
// Length of completion should equal length of text in decorator. draft-js
|
||||||
|
// relies on the length of the entity === length of the text in the decoration.
|
||||||
|
completion: user.rawDisplayName.replace(' (IRC)', ''),
|
||||||
suffix: range.start === 0 ? ': ' : ' ',
|
suffix: range.start === 0 ? ': ' : ' ',
|
||||||
href: 'https://matrix.to/#/' + user.userId,
|
href: 'https://matrix.to/#/' + user.userId,
|
||||||
component: (
|
component: (
|
||||||
|
|
|
@ -266,7 +266,7 @@ export default React.createClass({
|
||||||
this.setState({uploadingAvatar: false});
|
this.setState({uploadingAvatar: false});
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to upload avatar image", e);
|
console.error("Failed to upload avatar image", e);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to upload image', '', ErrorDialog, {
|
||||||
title: _t('Error'),
|
title: _t('Error'),
|
||||||
description: _t('Failed to upload image'),
|
description: _t('Failed to upload image'),
|
||||||
});
|
});
|
||||||
|
@ -288,7 +288,7 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to save group profile", e);
|
console.error("Failed to save group profile", e);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to update group', '', ErrorDialog, {
|
||||||
title: _t('Error'),
|
title: _t('Error'),
|
||||||
description: _t('Failed to update group'),
|
description: _t('Failed to update group'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -301,13 +301,13 @@ export default React.createClass({
|
||||||
|
|
||||||
case PageTypes.UserView:
|
case PageTypes.UserView:
|
||||||
page_element = null; // deliberately null for now
|
page_element = null; // deliberately null for now
|
||||||
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
|
right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
case PageTypes.GroupView:
|
case PageTypes.GroupView:
|
||||||
page_element = <GroupView
|
page_element = <GroupView
|
||||||
groupId={this.props.currentGroupId}
|
groupId={this.props.currentGroupId}
|
||||||
/>;
|
/>;
|
||||||
//right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
|
//right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -131,9 +131,6 @@ module.exports = React.createClass({
|
||||||
// the master view we are showing.
|
// the master view we are showing.
|
||||||
view: VIEWS.LOADING,
|
view: VIEWS.LOADING,
|
||||||
|
|
||||||
// a thing to call showScreen with once login completes.
|
|
||||||
screenAfterLogin: this.props.initialScreenAfterLogin,
|
|
||||||
|
|
||||||
// What the LoggedInView would be showing if visible
|
// What the LoggedInView would be showing if visible
|
||||||
page_type: null,
|
page_type: null,
|
||||||
|
|
||||||
|
@ -147,8 +144,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
collapse_lhs: false,
|
collapse_lhs: false,
|
||||||
collapse_rhs: false,
|
collapse_rhs: false,
|
||||||
ready: false,
|
|
||||||
width: 10000,
|
|
||||||
leftOpacity: 1.0,
|
leftOpacity: 1.0,
|
||||||
middleOpacity: 1.0,
|
middleOpacity: 1.0,
|
||||||
rightOpacity: 1.0,
|
rightOpacity: 1.0,
|
||||||
|
@ -274,6 +269,15 @@ module.exports = React.createClass({
|
||||||
register_hs_url: paramHs,
|
register_hs_url: paramHs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a thing to call showScreen with once login completes. this is kept
|
||||||
|
// outside this.state because updating it should never trigger a
|
||||||
|
// rerender.
|
||||||
|
this._screenAfterLogin = this.props.initialScreenAfterLogin;
|
||||||
|
|
||||||
|
this._windowWidth = 10000;
|
||||||
|
this.handleResize();
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -294,9 +298,6 @@ module.exports = React.createClass({
|
||||||
linkifyMatrix.onGroupClick = this.onGroupClick;
|
linkifyMatrix.onGroupClick = this.onGroupClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
|
||||||
this.handleResize();
|
|
||||||
|
|
||||||
const teamServerConfig = this.props.config.teamServerConfig || {};
|
const teamServerConfig = this.props.config.teamServerConfig || {};
|
||||||
Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
|
Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
|
||||||
|
|
||||||
|
@ -312,13 +313,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// if the user has followed a login or register link, don't reanimate
|
// if the user has followed a login or register link, don't reanimate
|
||||||
// the old creds, but rather go straight to the relevant page
|
// the old creds, but rather go straight to the relevant page
|
||||||
const firstScreen = this.state.screenAfterLogin ?
|
const firstScreen = this._screenAfterLogin ?
|
||||||
this.state.screenAfterLogin.screen : null;
|
this._screenAfterLogin.screen : null;
|
||||||
|
|
||||||
if (firstScreen === 'login' ||
|
if (firstScreen === 'login' ||
|
||||||
firstScreen === 'register' ||
|
firstScreen === 'register' ||
|
||||||
firstScreen === 'forgot_password') {
|
firstScreen === 'forgot_password') {
|
||||||
this.setState({loading: false});
|
|
||||||
this._showScreenAfterLogin();
|
this._showScreenAfterLogin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -367,9 +367,9 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
const newState = {
|
const newState = {
|
||||||
viewUserId: null,
|
viewUserId: null,
|
||||||
};
|
};
|
||||||
Object.assign(newState, state);
|
Object.assign(newState, state);
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
|
@ -410,7 +410,7 @@ module.exports = React.createClass({
|
||||||
this._leaveRoom(payload.room_id);
|
this._leaveRoom(payload.room_id);
|
||||||
break;
|
break;
|
||||||
case 'reject_invite':
|
case 'reject_invite':
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, {
|
||||||
title: _t('Reject invitation'),
|
title: _t('Reject invitation'),
|
||||||
description: _t('Are you sure you want to reject the invitation?'),
|
description: _t('Are you sure you want to reject the invitation?'),
|
||||||
onFinished: (confirm) => {
|
onFinished: (confirm) => {
|
||||||
|
@ -426,7 +426,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
modal.close();
|
modal.close();
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to reject invitation', '', ErrorDialog, {
|
||||||
title: _t('Failed to reject invitation'),
|
title: _t('Failed to reject invitation'),
|
||||||
description: err.toString(),
|
description: err.toString(),
|
||||||
});
|
});
|
||||||
|
@ -728,7 +728,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_setMxId: function(payload) {
|
_setMxId: function(payload) {
|
||||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||||
const close = Modal.createDialog(SetMxIdDialog, {
|
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||||
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
|
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
onFinished: (submitted, credentials) => {
|
onFinished: (submitted, credentials) => {
|
||||||
if (!submitted) {
|
if (!submitted) {
|
||||||
|
@ -767,7 +767,7 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
||||||
Modal.createDialog(ChatInviteDialog, {
|
Modal.createTrackedDialog('Start a chat', '', ChatInviteDialog, {
|
||||||
title: _t('Start a chat'),
|
title: _t('Start a chat'),
|
||||||
description: _t("Who would you like to communicate with?"),
|
description: _t("Who would you like to communicate with?"),
|
||||||
placeholder: _t("Email, name or matrix ID"),
|
placeholder: _t("Email, name or matrix ID"),
|
||||||
|
@ -787,7 +787,7 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||||
Modal.createDialog(TextInputDialog, {
|
Modal.createTrackedDialog('Create Room', '', TextInputDialog, {
|
||||||
title: _t('Create Room'),
|
title: _t('Create Room'),
|
||||||
description: _t('Room name (optional)'),
|
description: _t('Room name (optional)'),
|
||||||
button: _t('Create Room'),
|
button: _t('Create Room'),
|
||||||
|
@ -831,7 +831,7 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
|
const close = Modal.createTrackedDialog('Chat create or reuse', '', ChatCreateOrReuseDialog, {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
onFinished: (success) => {
|
onFinished: (success) => {
|
||||||
if (!success && goHomeOnCancel) {
|
if (!success && goHomeOnCancel) {
|
||||||
|
@ -859,7 +859,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_invite: function(roomId) {
|
_invite: function(roomId) {
|
||||||
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
||||||
Modal.createDialog(ChatInviteDialog, {
|
Modal.createTrackedDialog('Chat Invite', '', ChatInviteDialog, {
|
||||||
title: _t('Invite new room members'),
|
title: _t('Invite new room members'),
|
||||||
description: _t('Who would you like to add to this room?'),
|
description: _t('Who would you like to add to this room?'),
|
||||||
button: _t('Send Invites'),
|
button: _t('Send Invites'),
|
||||||
|
@ -873,7 +873,7 @@ module.exports = React.createClass({
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
|
||||||
title: _t("Leave room"),
|
title: _t("Leave room"),
|
||||||
description: (
|
description: (
|
||||||
<span>
|
<span>
|
||||||
|
@ -896,7 +896,7 @@ module.exports = React.createClass({
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
modal.close();
|
modal.close();
|
||||||
console.error("Failed to leave room " + roomId + " " + err);
|
console.error("Failed to leave room " + roomId + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
|
||||||
title: _t("Failed to leave room"),
|
title: _t("Failed to leave room"),
|
||||||
description: (err && err.message ? err.message :
|
description: (err && err.message ? err.message :
|
||||||
_t("Server may be unavailable, overloaded, or you hit a bug.")),
|
_t("Server may be unavailable, overloaded, or you hit a bug.")),
|
||||||
|
@ -992,14 +992,12 @@ module.exports = React.createClass({
|
||||||
_showScreenAfterLogin: function() {
|
_showScreenAfterLogin: function() {
|
||||||
// If screenAfterLogin is set, use that, then null it so that a second login will
|
// If screenAfterLogin is set, use that, then null it so that a second login will
|
||||||
// result in view_home_page, _user_settings or _room_directory
|
// result in view_home_page, _user_settings or _room_directory
|
||||||
if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) {
|
if (this._screenAfterLogin && this._screenAfterLogin.screen) {
|
||||||
this.showScreen(
|
this.showScreen(
|
||||||
this.state.screenAfterLogin.screen,
|
this._screenAfterLogin.screen,
|
||||||
this.state.screenAfterLogin.params,
|
this._screenAfterLogin.params,
|
||||||
);
|
);
|
||||||
// XXX: is this necessary? `showScreen` should do it for us.
|
this._screenAfterLogin = null;
|
||||||
this.notifyNewScreen(this.state.screenAfterLogin.screen);
|
|
||||||
this.setState({screenAfterLogin: null});
|
|
||||||
} else if (localStorage && localStorage.getItem('mx_last_room_id')) {
|
} else if (localStorage && localStorage.getItem('mx_last_room_id')) {
|
||||||
// Before defaulting to directory, show the last viewed room
|
// Before defaulting to directory, show the last viewed room
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -1092,7 +1090,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
cli.on('Session.logged_out', function(call) {
|
cli.on('Session.logged_out', function(call) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Signed out', '', ErrorDialog, {
|
||||||
title: _t('Signed Out'),
|
title: _t('Signed Out'),
|
||||||
description: _t('For security, this session has been signed out. Please sign in again.'),
|
description: _t('For security, this session has been signed out. Please sign in again.'),
|
||||||
});
|
});
|
||||||
|
@ -1205,21 +1203,24 @@ module.exports = React.createClass({
|
||||||
} else if (screen.indexOf('user/') == 0) {
|
} else if (screen.indexOf('user/') == 0) {
|
||||||
const userId = screen.substring(5);
|
const userId = screen.substring(5);
|
||||||
|
|
||||||
if (params.action === 'chat') {
|
// Wait for the first sync so that `getRoom` gives us a room object if it's
|
||||||
this._chatCreateOrReuse(userId);
|
// in the sync response
|
||||||
return;
|
const waitFor = this.firstSyncPromise ?
|
||||||
}
|
this.firstSyncPromise.promise : Promise.resolve();
|
||||||
|
waitFor.then(() => {
|
||||||
|
if (params.action === 'chat') {
|
||||||
|
this._chatCreateOrReuse(userId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ viewUserId: userId });
|
this._setPage(PageTypes.UserView);
|
||||||
this._setPage(PageTypes.UserView);
|
this.notifyNewScreen('user/' + userId);
|
||||||
this.notifyNewScreen('user/' + userId);
|
const member = new Matrix.RoomMember(null, userId);
|
||||||
const member = new Matrix.RoomMember(null, userId);
|
|
||||||
if (member) {
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_user',
|
action: 'view_user',
|
||||||
member: member,
|
member: member,
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
} else if (screen.indexOf('group/') == 0) {
|
} else if (screen.indexOf('group/') == 0) {
|
||||||
const groupId = screen.substring(6);
|
const groupId = screen.substring(6);
|
||||||
|
|
||||||
|
@ -1276,20 +1277,20 @@ module.exports = React.createClass({
|
||||||
const hideRhsThreshold = 820;
|
const hideRhsThreshold = 820;
|
||||||
const showRhsThreshold = 820;
|
const showRhsThreshold = 820;
|
||||||
|
|
||||||
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
||||||
dis.dispatch({ action: 'hide_left_panel' });
|
dis.dispatch({ action: 'hide_left_panel' });
|
||||||
}
|
}
|
||||||
if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
||||||
dis.dispatch({ action: 'show_left_panel' });
|
dis.dispatch({ action: 'show_left_panel' });
|
||||||
}
|
}
|
||||||
if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
|
if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
|
||||||
dis.dispatch({ action: 'hide_right_panel' });
|
dis.dispatch({ action: 'hide_right_panel' });
|
||||||
}
|
}
|
||||||
if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
|
if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
|
||||||
dis.dispatch({ action: 'show_right_panel' });
|
dis.dispatch({ action: 'show_right_panel' });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({width: window.innerWidth});
|
this._windowWidth = window.innerWidth;
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomCreated: function(roomId) {
|
onRoomCreated: function(roomId) {
|
||||||
|
|
|
@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var ReactDOM = require("react-dom");
|
import ReactDOM from 'react-dom';
|
||||||
var dis = require("../../dispatcher");
|
import UserSettingsStore from '../../UserSettingsStore';
|
||||||
var sdk = require('../../index');
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
|
import dis from "../../dispatcher";
|
||||||
|
import sdk from '../../index';
|
||||||
|
|
||||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
|
||||||
const MILLIS_IN_DAY = 86400000;
|
const MILLIS_IN_DAY = 86400000;
|
||||||
|
|
||||||
|
@ -90,9 +92,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// show timestamps always
|
// show timestamps always
|
||||||
alwaysShowTimestamps: React.PropTypes.bool,
|
alwaysShowTimestamps: React.PropTypes.bool,
|
||||||
|
|
||||||
// hide redacted events as per old behaviour
|
|
||||||
hideRedactions: React.PropTypes.bool,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -113,6 +112,8 @@ module.exports = React.createClass({
|
||||||
// Velocity requires
|
// Velocity requires
|
||||||
this._readMarkerGhostNode = null;
|
this._readMarkerGhostNode = null;
|
||||||
|
|
||||||
|
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
|
|
||||||
this._isMounted = true;
|
this._isMounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -238,8 +239,20 @@ module.exports = React.createClass({
|
||||||
return !this._isMounted;
|
return !this._isMounted;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getEventTiles: function() {
|
// TODO: Implement granular (per-room) hide options
|
||||||
|
_shouldShowEvent: function(mxEv) {
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
|
return false; // no tile = no show
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show highlighted event
|
||||||
|
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||||
|
|
||||||
|
return !shouldHideEvent(mxEv, this._syncedSettings);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getEventTiles: function() {
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||||
|
|
||||||
|
@ -249,20 +262,21 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// first figure out which is the last event in the list which we're
|
// first figure out which is the last event in the list which we're
|
||||||
// actually going to show; this allows us to behave slightly
|
// actually going to show; this allows us to behave slightly
|
||||||
// differently for the last event in the list.
|
// differently for the last event in the list. (eg show timestamp)
|
||||||
//
|
//
|
||||||
// we also need to figure out which is the last event we show which isn't
|
// we also need to figure out which is the last event we show which isn't
|
||||||
// a local echo, to manage the read-marker.
|
// a local echo, to manage the read-marker.
|
||||||
var lastShownEventIndex = -1;
|
let lastShownEvent;
|
||||||
|
|
||||||
var lastShownNonLocalEchoIndex = -1;
|
var lastShownNonLocalEchoIndex = -1;
|
||||||
for (i = this.props.events.length-1; i >= 0; i--) {
|
for (i = this.props.events.length-1; i >= 0; i--) {
|
||||||
var mxEv = this.props.events[i];
|
var mxEv = this.props.events[i];
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!this._shouldShowEvent(mxEv)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastShownEventIndex < 0) {
|
if (lastShownEvent === undefined) {
|
||||||
lastShownEventIndex = i;
|
lastShownEvent = mxEv;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mxEv.status) {
|
if (mxEv.status) {
|
||||||
|
@ -288,25 +302,18 @@ module.exports = React.createClass({
|
||||||
this.currentGhostEventId = null;
|
this.currentGhostEventId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isMembershipChange = (e) => e.getType() === 'm.room.member';
|
const isMembershipChange = (e) => e.getType() === 'm.room.member';
|
||||||
|
|
||||||
for (i = 0; i < this.props.events.length; i++) {
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
let mxEv = this.props.events[i];
|
let mxEv = this.props.events[i];
|
||||||
let wantTile = true;
|
|
||||||
let eventId = mxEv.getId();
|
let eventId = mxEv.getId();
|
||||||
let readMarkerInMels = false;
|
let last = (mxEv === lastShownEvent);
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
const wantTile = this._shouldShowEvent(mxEv);
|
||||||
wantTile = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let last = (i == lastShownEventIndex);
|
|
||||||
|
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
if (isMembershipChange(mxEv) &&
|
if (isMembershipChange(mxEv) && wantTile) {
|
||||||
EventTile.haveTileForEvent(mxEv) &&
|
let readMarkerInMels = false;
|
||||||
!mxEv.isRedacted()
|
|
||||||
) {
|
|
||||||
let ts1 = mxEv.getTs();
|
let ts1 = mxEv.getTs();
|
||||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||||
// member events. This will prevent it from being re-created unnecessarily, and
|
// member events. This will prevent it from being re-created unnecessarily, and
|
||||||
|
@ -323,37 +330,43 @@ module.exports = React.createClass({
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If RM event is the first in the MELS, append the RM after MELS
|
||||||
|
if (mxEv.getId() === this.props.readMarkerEventId) {
|
||||||
|
readMarkerInMels = true;
|
||||||
|
}
|
||||||
|
|
||||||
let summarisedEvents = [mxEv];
|
let summarisedEvents = [mxEv];
|
||||||
for (;i + 1 < this.props.events.length; i++) {
|
for (;i + 1 < this.props.events.length; i++) {
|
||||||
let collapsedMxEv = this.props.events[i + 1];
|
const collapsedMxEv = this.props.events[i + 1];
|
||||||
|
|
||||||
// Ignore redacted member events
|
|
||||||
if (!EventTile.haveTileForEvent(collapsedMxEv)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMembershipChange(collapsedMxEv) ||
|
if (!isMembershipChange(collapsedMxEv) ||
|
||||||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If RM event is in MELS mark it as such and the RM will be appended after MELS.
|
||||||
|
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||||
|
readMarkerInMels = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore redacted/hidden member events
|
||||||
|
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
summarisedEvents.push(collapsedMxEv);
|
summarisedEvents.push(collapsedMxEv);
|
||||||
}
|
}
|
||||||
// At this point, i = the index of the last event in the summary sequence
|
|
||||||
|
|
||||||
let eventTiles = summarisedEvents.map(
|
// At this point, i = the index of the last event in the summary sequence
|
||||||
(e) => {
|
let eventTiles = summarisedEvents.map((e) => {
|
||||||
if (e.getId() === this.props.readMarkerEventId) {
|
// In order to prevent DateSeparators from appearing in the expanded form
|
||||||
readMarkerInMels = true;
|
// of MemberEventListSummary, render each member event as if the previous
|
||||||
}
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
// In order to prevent DateSeparators from appearing in the expanded form
|
// timestamp of the current event, and no DateSeperator is inserted.
|
||||||
// of MemberEventListSummary, render each member event as if the previous
|
const ret = this._getTilesForEvent(e, e, e === lastShownEvent);
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
prevEvent = e;
|
||||||
// timestamp of the current event, and no DateSeperator is inserted.
|
return ret;
|
||||||
let ret = this._getTilesForEvent(e, e);
|
}).reduce((a, b) => a.concat(b));
|
||||||
prevEvent = e;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
).reduce((a, b) => a.concat(b));
|
|
||||||
|
|
||||||
if (eventTiles.length === 0) {
|
if (eventTiles.length === 0) {
|
||||||
eventTiles = null;
|
eventTiles = null;
|
||||||
|
@ -466,8 +479,6 @@ module.exports = React.createClass({
|
||||||
continuation = false;
|
continuation = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mxEv.isRedacted() && this.props.hideRedactions) return ret;
|
|
||||||
|
|
||||||
var eventId = mxEv.getId();
|
var eventId = mxEv.getId();
|
||||||
var highlight = (eventId == this.props.highlightedEventId);
|
var highlight = (eventId == this.props.highlightedEventId);
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ export default withMatrixClient(React.createClass({
|
||||||
|
|
||||||
_onCreateGroupClick: function() {
|
_onCreateGroupClick: function() {
|
||||||
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
||||||
Modal.createDialog(CreateGroupDialog);
|
Modal.createTrackedDialog('Create Group', '', CreateGroupDialog);
|
||||||
},
|
},
|
||||||
|
|
||||||
_fetch: function() {
|
_fetch: function() {
|
||||||
|
|
|
@ -544,7 +544,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
if (!userHasUsedEncryption) {
|
if (!userHasUsedEncryption) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('E2E Warning', '', QuestionDialog, {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
hasCancelButton: false,
|
hasCancelButton: false,
|
||||||
description: (
|
description: (
|
||||||
|
@ -820,7 +820,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||||
const close = Modal.createDialog(SetMxIdDialog, {
|
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||||
homeserverUrl: cli.getHomeserverUrl(),
|
homeserverUrl: cli.getHomeserverUrl(),
|
||||||
onFinished: (submitted, credentials) => {
|
onFinished: (submitted, credentials) => {
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
|
@ -934,7 +934,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to upload file " + file + " " + error);
|
console.error("Failed to upload file " + file + " " + error);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
|
||||||
title: _t('Failed to upload file'),
|
title: _t('Failed to upload file'),
|
||||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
||||||
});
|
});
|
||||||
|
@ -1021,7 +1021,7 @@ module.exports = React.createClass({
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Search failed: " + error);
|
console.error("Search failed: " + error);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
|
||||||
title: _t("Search failed"),
|
title: _t("Search failed"),
|
||||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
|
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
|
||||||
});
|
});
|
||||||
|
@ -1148,7 +1148,7 @@ module.exports = React.createClass({
|
||||||
console.error(result.reason);
|
console.error(result.reason);
|
||||||
});
|
});
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to save room settings', '', ErrorDialog, {
|
||||||
title: _t("Failed to save settings"),
|
title: _t("Failed to save settings"),
|
||||||
description: fails.map(function(result) { return result.reason; }).join("\n"),
|
description: fails.map(function(result) { return result.reason; }).join("\n"),
|
||||||
});
|
});
|
||||||
|
@ -1195,7 +1195,7 @@ module.exports = React.createClass({
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
var errCode = err.errcode || _t("unknown error code");
|
var errCode = err.errcode || _t("unknown error code");
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
|
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
|
||||||
});
|
});
|
||||||
|
@ -1217,7 +1217,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var msg = error.message ? error.message : JSON.stringify(error);
|
var msg = error.message ? error.message : JSON.stringify(error);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
|
||||||
title: _t("Failed to reject invite"),
|
title: _t("Failed to reject invite"),
|
||||||
description: msg,
|
description: msg,
|
||||||
});
|
});
|
||||||
|
|
|
@ -181,9 +181,6 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// always show timestamps on event tiles?
|
// always show timestamps on event tiles?
|
||||||
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
|
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
|
||||||
|
|
||||||
// hide redacted events as per old behaviour
|
|
||||||
hideRedactions: syncedSettings.hideRedactions,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -926,7 +923,7 @@ var TimelinePanel = React.createClass({
|
||||||
var message = (error.errcode == 'M_FORBIDDEN')
|
var message = (error.errcode == 'M_FORBIDDEN')
|
||||||
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
|
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
|
||||||
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
|
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
|
||||||
title: _t("Failed to load timeline position"),
|
title: _t("Failed to load timeline position"),
|
||||||
description: message,
|
description: message,
|
||||||
onFinished: onFinished,
|
onFinished: onFinished,
|
||||||
|
@ -1122,7 +1119,6 @@ var TimelinePanel = React.createClass({
|
||||||
return (
|
return (
|
||||||
<MessagePanel ref="messagePanel"
|
<MessagePanel ref="messagePanel"
|
||||||
hidden={ this.props.hidden }
|
hidden={ this.props.hidden }
|
||||||
hideRedactions={ this.state.hideRedactions }
|
|
||||||
backPaginating={ this.state.backPaginating }
|
backPaginating={ this.state.backPaginating }
|
||||||
forwardPaginating={ forwardPaginating }
|
forwardPaginating={ forwardPaginating }
|
||||||
events={ this.state.events }
|
events={ this.state.events }
|
||||||
|
|
|
@ -81,6 +81,14 @@ const SETTINGS_LABELS = [
|
||||||
id: 'showTwelveHourTimestamps',
|
id: 'showTwelveHourTimestamps',
|
||||||
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'hideJoinLeaves',
|
||||||
|
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideAvatarDisplaynameChanges',
|
||||||
|
label: 'Hide avatar and display name changes',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'useCompactLayout',
|
id: 'useCompactLayout',
|
||||||
label: 'Use compact timeline layout',
|
label: 'Use compact timeline layout',
|
||||||
|
@ -97,6 +105,10 @@ const SETTINGS_LABELS = [
|
||||||
id: 'MessageComposerInput.autoReplaceEmoji',
|
id: 'MessageComposerInput.autoReplaceEmoji',
|
||||||
label: 'Automatically replace plain text Emoji',
|
label: 'Automatically replace plain text Emoji',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'Pill.shouldHidePillAvatar',
|
||||||
|
label: 'Hide avatars in user and room mentions',
|
||||||
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
id: 'useFixedWidthFont',
|
id: 'useFixedWidthFont',
|
||||||
|
@ -327,7 +339,7 @@ module.exports = React.createClass({
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to load user settings: " + error);
|
console.error("Failed to load user settings: " + error);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Can\'t load user settings', '', ErrorDialog, {
|
||||||
title: _t("Can't load user settings"),
|
title: _t("Can't load user settings"),
|
||||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")),
|
description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")),
|
||||||
});
|
});
|
||||||
|
@ -360,7 +372,7 @@ module.exports = React.createClass({
|
||||||
// const errMsg = (typeof err === "string") ? err : (err.error || "");
|
// const errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||||
console.error("Failed to set avatar: " + err);
|
console.error("Failed to set avatar: " + err);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, {
|
||||||
title: _t("Failed to set avatar."),
|
title: _t("Failed to set avatar."),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -369,7 +381,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onLogoutClicked: function(ev) {
|
onLogoutClicked: function(ev) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
|
||||||
title: _t("Sign out"),
|
title: _t("Sign out"),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
@ -405,7 +417,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to change password: " + errMsg);
|
console.error("Failed to change password: " + errMsg);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: errMsg,
|
description: errMsg,
|
||||||
});
|
});
|
||||||
|
@ -413,7 +425,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onPasswordChanged: function() {
|
onPasswordChanged: function() {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
|
||||||
title: _t("Success"),
|
title: _t("Success"),
|
||||||
description: _t(
|
description: _t(
|
||||||
"Your password was successfully changed. You will not receive " +
|
"Your password was successfully changed. You will not receive " +
|
||||||
|
@ -438,7 +450,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const emailAddress = this.refs.add_email_input.value;
|
const emailAddress = this.refs.add_email_input.value;
|
||||||
if (!Email.looksValid(emailAddress)) {
|
if (!Email.looksValid(emailAddress)) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, {
|
||||||
title: _t("Invalid Email Address"),
|
title: _t("Invalid Email Address"),
|
||||||
description: _t("This doesn't appear to be a valid email address"),
|
description: _t("This doesn't appear to be a valid email address"),
|
||||||
});
|
});
|
||||||
|
@ -448,7 +460,7 @@ module.exports = React.createClass({
|
||||||
// we always bind emails when registering, so let's do the
|
// we always bind emails when registering, so let's do the
|
||||||
// same here.
|
// same here.
|
||||||
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
|
||||||
title: _t("Verification Pending"),
|
title: _t("Verification Pending"),
|
||||||
description: _t(
|
description: _t(
|
||||||
"Please check your email and click on the link it contains. Once this " +
|
"Please check your email and click on the link it contains. Once this " +
|
||||||
|
@ -460,7 +472,7 @@ module.exports = React.createClass({
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.setState({email_add_pending: false});
|
this.setState({email_add_pending: false});
|
||||||
console.error("Unable to add email address " + emailAddress + " " + err);
|
console.error("Unable to add email address " + emailAddress + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
|
||||||
title: _t("Unable to add email address"),
|
title: _t("Unable to add email address"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -471,7 +483,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onRemoveThreepidClicked: function(threepid) {
|
onRemoveThreepidClicked: function(threepid) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Remove 3pid', '', QuestionDialog, {
|
||||||
title: _t("Remove Contact Information?"),
|
title: _t("Remove Contact Information?"),
|
||||||
description: _t("Remove %(threePid)s?", { threePid: threepid.address }),
|
description: _t("Remove %(threePid)s?", { threePid: threepid.address }),
|
||||||
button: _t('Remove'),
|
button: _t('Remove'),
|
||||||
|
@ -485,7 +497,7 @@ module.exports = React.createClass({
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Unable to remove contact information: " + err);
|
console.error("Unable to remove contact information: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||||
title: _t("Unable to remove contact information"),
|
title: _t("Unable to remove contact information"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -517,7 +529,7 @@ module.exports = React.createClass({
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
const message = _t("Unable to verify email address.") + " " +
|
const message = _t("Unable to verify email address.") + " " +
|
||||||
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
|
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
|
||||||
title: _t("Verification Pending"),
|
title: _t("Verification Pending"),
|
||||||
description: message,
|
description: message,
|
||||||
button: _t('Continue'),
|
button: _t('Continue'),
|
||||||
|
@ -526,7 +538,7 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Unable to verify email address: " + err);
|
console.error("Unable to verify email address: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
|
||||||
title: _t("Unable to verify email address."),
|
title: _t("Unable to verify email address."),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -536,7 +548,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_onDeactivateAccountClicked: function() {
|
_onDeactivateAccountClicked: function() {
|
||||||
const DeactivateAccountDialog = sdk.getComponent("dialogs.DeactivateAccountDialog");
|
const DeactivateAccountDialog = sdk.getComponent("dialogs.DeactivateAccountDialog");
|
||||||
Modal.createDialog(DeactivateAccountDialog, {});
|
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onBugReportClicked: function() {
|
_onBugReportClicked: function() {
|
||||||
|
@ -544,7 +556,7 @@ module.exports = React.createClass({
|
||||||
if (!BugReportDialog) {
|
if (!BugReportDialog) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Modal.createDialog(BugReportDialog, {});
|
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onClearCacheClicked: function() {
|
_onClearCacheClicked: function() {
|
||||||
|
@ -581,27 +593,23 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onExportE2eKeysClicked: function() {
|
_onExportE2eKeysClicked: function() {
|
||||||
Modal.createDialogAsync(
|
Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => {
|
||||||
(cb) => {
|
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||||
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||||
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
}, "e2e-export");
|
||||||
}, "e2e-export");
|
}, {
|
||||||
}, {
|
matrixClient: MatrixClientPeg.get(),
|
||||||
matrixClient: MatrixClientPeg.get(),
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_onImportE2eKeysClicked: function() {
|
_onImportE2eKeysClicked: function() {
|
||||||
Modal.createDialogAsync(
|
Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => {
|
||||||
(cb) => {
|
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
|
||||||
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
|
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
|
||||||
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
|
}, "e2e-export");
|
||||||
}, "e2e-export");
|
}, {
|
||||||
}, {
|
matrixClient: MatrixClientPeg.get(),
|
||||||
matrixClient: MatrixClientPeg.get(),
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderReferral: function() {
|
_renderReferral: function() {
|
||||||
|
@ -851,7 +859,13 @@ module.exports = React.createClass({
|
||||||
if (this.props.enableLabs === false) return null;
|
if (this.props.enableLabs === false) return null;
|
||||||
UserSettingsStore.doTranslations();
|
UserSettingsStore.doTranslations();
|
||||||
|
|
||||||
const features = UserSettingsStore.LABS_FEATURES.map((feature) => {
|
const features = [];
|
||||||
|
UserSettingsStore.LABS_FEATURES.forEach((feature) => {
|
||||||
|
// This feature has an override and will be set to the default, so do not
|
||||||
|
// show it here.
|
||||||
|
if (feature.override) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: this ought to be a separate component so that we don't need
|
// TODO: this ought to be a separate component so that we don't need
|
||||||
// to rebind the onChange each time we render
|
// to rebind the onChange each time we render
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
|
@ -859,7 +873,7 @@ module.exports = React.createClass({
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
features.push(
|
||||||
<div key={feature.id} className="mx_UserSettings_toggle">
|
<div key={feature.id} className="mx_UserSettings_toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -869,9 +883,14 @@ module.exports = React.createClass({
|
||||||
onChange={ onChange }
|
onChange={ onChange }
|
||||||
/>
|
/>
|
||||||
<label htmlFor={feature.id}>{feature.name}</label>
|
<label htmlFor={feature.id}>{feature.name}</label>
|
||||||
</div>
|
</div>);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// No labs section when there are no features in labs
|
||||||
|
if (features.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t("Labs") }</h3>
|
<h3>{ _t("Labs") }</h3>
|
||||||
|
@ -1000,7 +1019,7 @@ module.exports = React.createClass({
|
||||||
this._refreshMediaDevices,
|
this._refreshMediaDevices,
|
||||||
function() {
|
function() {
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
||||||
title: _t('No media permissions'),
|
title: _t('No media permissions'),
|
||||||
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
|
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
|
||||||
});
|
});
|
||||||
|
@ -1136,7 +1155,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const threepidsSection = this.state.threepids.map((val, pidIndex) => {
|
const threepidsSection = this.state.threepids.map((val, pidIndex) => {
|
||||||
const id = "3pid-" + val.address;
|
const id = "3pid-" + val.address;
|
||||||
// TODO; make a separate component to avoid having to rebind onClick
|
// TODO: make a separate component to avoid having to rebind onClick
|
||||||
// each time we render
|
// each time we render
|
||||||
const onRemoveClick = (e) => this.onRemoveThreepidClicked(val);
|
const onRemoveClick = (e) => this.onRemoveThreepidClicked(val);
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -89,14 +89,14 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
|
||||||
title: _t('Warning!'),
|
title: _t('Warning!'),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
{ _t(
|
{ _t(
|
||||||
'Resetting password will currently reset any ' +
|
'Resetting password will currently reset any ' +
|
||||||
'end-to-end encryption keys on all devices, ' +
|
'end-to-end encryption keys on all devices, ' +
|
||||||
'making encrypted chat history unreadable, ' +
|
'making encrypted chat history unreadable, ' +
|
||||||
'unless you first export your room keys and re-import ' +
|
'unless you first export your room keys and re-import ' +
|
||||||
'them afterwards. In future this will be improved.'
|
'them afterwards. In future this will be improved.'
|
||||||
) }
|
) }
|
||||||
|
@ -121,15 +121,13 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onExportE2eKeysClicked: function() {
|
_onExportE2eKeysClicked: function() {
|
||||||
Modal.createDialogAsync(
|
Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password', (cb) => {
|
||||||
(cb) => {
|
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||||
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||||
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
}, "e2e-export");
|
||||||
}, "e2e-export");
|
}, {
|
||||||
}, {
|
matrixClient: MatrixClientPeg.get(),
|
||||||
matrixClient: MatrixClientPeg.get(),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputChanged: function(stateKey, ev) {
|
onInputChanged: function(stateKey, ev) {
|
||||||
|
@ -152,7 +150,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
showErrorDialog: function(body, title) {
|
showErrorDialog: function(body, title) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
|
||||||
title: title,
|
title: title,
|
||||||
description: body,
|
description: body,
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,8 +19,11 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t, _tJsx } from '../../../languageHandler';
|
||||||
|
import * as languageHandler from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Login from '../../../Login';
|
import Login from '../../../Login';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
|
|
||||||
// For validating phone numbers without country codes
|
// For validating phone numbers without country codes
|
||||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||||
|
@ -306,6 +309,23 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onLanguageChange: function(newLang) {
|
||||||
|
if(languageHandler.getCurrentLanguage() !== newLang) {
|
||||||
|
UserSettingsStore.setLocalSetting('language', newLang);
|
||||||
|
PlatformPeg.get().reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderLanguageSetting: function() {
|
||||||
|
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||||
|
return <div className="mx_Login_language_div">
|
||||||
|
<LanguageDropdown onOptionChange={this._onLanguageChange}
|
||||||
|
className="mx_Login_language"
|
||||||
|
value={languageHandler.getCurrentLanguage()}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||||
|
@ -354,6 +374,7 @@ module.exports = React.createClass({
|
||||||
</a>
|
</a>
|
||||||
{ loginAsGuestJsx }
|
{ loginAsGuestJsx }
|
||||||
{ returnToAppJsx }
|
{ returnToAppJsx }
|
||||||
|
{ this._renderLanguageSetting() }
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -103,7 +103,7 @@ module.exports = React.createClass({
|
||||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||||
"views.dialogs.ChatCreateOrReuseDialog",
|
"views.dialogs.ChatCreateOrReuseDialog",
|
||||||
);
|
);
|
||||||
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
|
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
onFinished: (success) => {
|
onFinished: (success) => {
|
||||||
this.props.onFinished(success);
|
this.props.onFinished(success);
|
||||||
|
@ -367,7 +367,7 @@ module.exports = React.createClass({
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite"),
|
title: _t("Failed to invite"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -380,7 +380,7 @@ module.exports = React.createClass({
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite user"),
|
title: _t("Failed to invite user"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -401,7 +401,7 @@ module.exports = React.createClass({
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite"),
|
title: _t("Failed to invite"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -448,7 +448,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (errorList.length > 0) {
|
if (errorList.length > 0) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
||||||
description: errorList.join(", "),
|
description: errorList.join(", "),
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import Analytics from '../../../Analytics';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import * as Lifecycle from '../../../Lifecycle';
|
import * as Lifecycle from '../../../Lifecycle';
|
||||||
import Velocity from 'velocity-vector';
|
import Velocity from 'velocity-vector';
|
||||||
|
@ -54,6 +55,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
user: MatrixClientPeg.get().credentials.userId,
|
user: MatrixClientPeg.get().credentials.userId,
|
||||||
password: this._passwordField.value,
|
password: this._passwordField.value,
|
||||||
}).done(() => {
|
}).done(() => {
|
||||||
|
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||||
Lifecycle.onLoggedOut();
|
Lifecycle.onLoggedOut();
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Usage:
|
* Usage:
|
||||||
* Modal.createDialog(ErrorDialog, {
|
* Modal.createTrackedDialog('An Identifier', 'some detail', ErrorDialog, {
|
||||||
* title: "some text", (default: "Error")
|
* title: "some text", (default: "Error")
|
||||||
* description: "some more text",
|
* description: "some more text",
|
||||||
* button: "Button Text",
|
* button: "Button Text",
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default React.createClass({
|
||||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||||
|
|
||||||
console.log("KeyShareDialog: Starting verify dialog");
|
console.log("KeyShareDialog: Starting verify dialog");
|
||||||
Modal.createDialog(DeviceVerifyDialog, {
|
Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
|
||||||
userId: this.props.userId,
|
userId: this.props.userId,
|
||||||
device: this.state.deviceInfo,
|
device: this.state.deviceInfo,
|
||||||
onFinished: (verified) => {
|
onFinished: (verified) => {
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default React.createClass({
|
||||||
|
|
||||||
_sendBugReport: function() {
|
_sendBugReport: function() {
|
||||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||||
Modal.createDialog(BugReportDialog, {});
|
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
_continueClicked: function() {
|
_continueClicked: function() {
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default React.createClass({
|
||||||
|
|
||||||
const emailAddress = this.state.emailAddress;
|
const emailAddress = this.state.emailAddress;
|
||||||
if (!Email.looksValid(emailAddress)) {
|
if (!Email.looksValid(emailAddress)) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, {
|
||||||
title: _t("Invalid Email Address"),
|
title: _t("Invalid Email Address"),
|
||||||
description: _t("This doesn't appear to be a valid email address"),
|
description: _t("This doesn't appear to be a valid email address"),
|
||||||
});
|
});
|
||||||
|
@ -65,7 +65,7 @@ export default React.createClass({
|
||||||
// we always bind emails when registering, so let's do the
|
// we always bind emails when registering, so let's do the
|
||||||
// same here.
|
// same here.
|
||||||
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
|
||||||
title: _t("Verification Pending"),
|
title: _t("Verification Pending"),
|
||||||
description: _t(
|
description: _t(
|
||||||
"Please check your email and click on the link it contains. Once this " +
|
"Please check your email and click on the link it contains. Once this " +
|
||||||
|
@ -77,7 +77,7 @@ export default React.createClass({
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.setState({emailBusy: false});
|
this.setState({emailBusy: false});
|
||||||
console.error("Unable to add email address " + emailAddress + " " + err);
|
console.error("Unable to add email address " + emailAddress + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
|
||||||
title: _t("Unable to add email address"),
|
title: _t("Unable to add email address"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -106,7 +106,7 @@ export default React.createClass({
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
const message = _t("Unable to verify email address.") + " " +
|
const message = _t("Unable to verify email address.") + " " +
|
||||||
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
|
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Verification Pending', '3pid Auth Failed', QuestionDialog, {
|
||||||
title: _t("Verification Pending"),
|
title: _t("Verification Pending"),
|
||||||
description: message,
|
description: message,
|
||||||
button: _t('Continue'),
|
button: _t('Continue'),
|
||||||
|
@ -115,7 +115,7 @@ export default React.createClass({
|
||||||
} else {
|
} else {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Unable to verify email address: " + err);
|
console.error("Unable to verify email address: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
|
||||||
title: _t("Unable to verify email address."),
|
title: _t("Unable to verify email address."),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
|
|
@ -106,6 +106,16 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_doUsernameCheck: function() {
|
_doUsernameCheck: function() {
|
||||||
|
// XXX: SPEC-1
|
||||||
|
// Check if username is valid
|
||||||
|
// Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190
|
||||||
|
if (encodeURIComponent(this.state.username) !== this.state.username) {
|
||||||
|
this.setState({
|
||||||
|
usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'),
|
||||||
|
});
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
// Check if username is available
|
// Check if username is available
|
||||||
return this._matrixClient.isUsernameAvailable(this.state.username).then(
|
return this._matrixClient.isUsernameAvailable(this.state.username).then(
|
||||||
(isAvailable) => {
|
(isAvailable) => {
|
||||||
|
@ -242,7 +252,7 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_SetMxIdDialog"
|
<BaseDialog className="mx_SetMxIdDialog"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title="To get started, please pick a username!"
|
title={_t('To get started, please pick a username!')}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_SetMxIdDialog_input_group">
|
<div className="mx_SetMxIdDialog_input_group">
|
||||||
|
|
75
src/components/views/elements/AppPermission.js
Normal file
75
src/components/views/elements/AppPermission.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import url from 'url';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
export default class AppPermission extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const curlBase = this.getCurlBase();
|
||||||
|
this.state = { curlBase: curlBase};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return string representation of content URL without query parameters
|
||||||
|
getCurlBase() {
|
||||||
|
const wurl = url.parse(this.props.url);
|
||||||
|
let curl;
|
||||||
|
let curlString;
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(wurl.search);
|
||||||
|
|
||||||
|
if(this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||||
|
curl = url.parse(searchParams.get('url'));
|
||||||
|
if(curl) {
|
||||||
|
curl.search = curl.query = "";
|
||||||
|
curlString = curl.format();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!curl && wurl) {
|
||||||
|
wurl.search = wurl.query = "";
|
||||||
|
curlString = wurl.format();
|
||||||
|
}
|
||||||
|
return curlString;
|
||||||
|
}
|
||||||
|
|
||||||
|
isScalarWurl(wurl) {
|
||||||
|
if(wurl && wurl.hostname && (
|
||||||
|
wurl.hostname === 'scalar.vector.im' ||
|
||||||
|
wurl.hostname === 'scalar-staging.riot.im' ||
|
||||||
|
wurl.hostname === 'scalar-develop.riot.im' ||
|
||||||
|
wurl.hostname === 'demo.riot.im' ||
|
||||||
|
wurl.hostname === 'localhost'
|
||||||
|
)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='mx_AppPermissionWarning'>
|
||||||
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
|
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||||
|
</div>
|
||||||
|
<div className='mx_AppPermissionWarningText'>
|
||||||
|
<span className='mx_AppPermissionWarningTextLabel'>Do you want to load widget from URL:</span> <span className='mx_AppPermissionWarningTextURL'>{this.state.curlBase}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className='mx_AppPermissionButton'
|
||||||
|
type='button'
|
||||||
|
value={_t('Allow')}
|
||||||
|
onClick={this.props.onPermissionGranted}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppPermission.propTypes = {
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
onPermissionGranted: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
AppPermission.defaultProps = {
|
||||||
|
onPermissionGranted: function() {},
|
||||||
|
};
|
|
@ -24,6 +24,10 @@ import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import AppPermission from './AppPermission';
|
||||||
|
import AppWarning from './AppWarning';
|
||||||
|
import MessageSpinner from './MessageSpinner';
|
||||||
|
import WidgetUtils from '../../../WidgetUtils';
|
||||||
|
|
||||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||||
const betaHelpMsg = 'This feature is currently experimental and is intended for beta testing only';
|
const betaHelpMsg = 'This feature is currently experimental and is intended for beta testing only';
|
||||||
|
@ -37,6 +41,9 @@ export default React.createClass({
|
||||||
name: React.PropTypes.string.isRequired,
|
name: React.PropTypes.string.isRequired,
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
type: React.PropTypes.string.isRequired,
|
type: React.PropTypes.string.isRequired,
|
||||||
|
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||||
|
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||||
|
fullWidth: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -46,9 +53,13 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_');
|
||||||
|
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
widgetUrl: this.props.url,
|
widgetUrl: this.props.url,
|
||||||
|
widgetPermissionId: widgetPermissionId,
|
||||||
|
hasPermissionToLoad: Boolean(hasPermissionToLoad === 'true'),
|
||||||
error: null,
|
error: null,
|
||||||
deleting: false,
|
deleting: false,
|
||||||
};
|
};
|
||||||
|
@ -60,6 +71,18 @@ export default React.createClass({
|
||||||
return scalarUrl && this.props.url.startsWith(scalarUrl);
|
return scalarUrl && this.props.url.startsWith(scalarUrl);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isMixedContent: function() {
|
||||||
|
const parentContentProtocol = window.location.protocol;
|
||||||
|
const u = url.parse(this.props.url);
|
||||||
|
const childContentProtocol = u.protocol;
|
||||||
|
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
|
||||||
|
console.warn("Refusing to load mixed-content app:",
|
||||||
|
parentContentProtocol, childContentProtocol, window.location, this.props.url);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
if (!this.isScalarUrl()) {
|
if (!this.isScalarUrl()) {
|
||||||
return;
|
return;
|
||||||
|
@ -71,6 +94,7 @@ export default React.createClass({
|
||||||
this._scalarClient = new ScalarAuthClient();
|
this._scalarClient = new ScalarAuthClient();
|
||||||
this._scalarClient.getScalarToken().done((token) => {
|
this._scalarClient.getScalarToken().done((token) => {
|
||||||
// Append scalar_token as a query param
|
// Append scalar_token as a query param
|
||||||
|
this._scalarClient.scalarToken = token;
|
||||||
const u = url.parse(this.props.url);
|
const u = url.parse(this.props.url);
|
||||||
if (!u.search) {
|
if (!u.search) {
|
||||||
u.search = "?scalar_token=" + encodeURIComponent(token);
|
u.search = "?scalar_token=" + encodeURIComponent(token);
|
||||||
|
@ -91,29 +115,62 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_canUserModify: function() {
|
||||||
|
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||||
|
},
|
||||||
|
|
||||||
_onEditClick: function(e) {
|
_onEditClick: function(e) {
|
||||||
console.log("Edit widget ID ", this.props.id);
|
console.log("Edit widget ID ", this.props.id);
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'type_' + this.props.type);
|
const src = this._scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'type_' + this.props.type);
|
||||||
Modal.createDialog(IntegrationsManager, {
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
src: src,
|
src: src,
|
||||||
}, "mx_IntegrationsManager");
|
}, "mx_IntegrationsManager");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* If user has permission to modify widgets, delete the widget, otherwise revoke access for the widget to load in the user's browser
|
||||||
|
*/
|
||||||
_onDeleteClick: function() {
|
_onDeleteClick: function() {
|
||||||
console.log("Delete widget %s", this.props.id);
|
if (this._canUserModify()) {
|
||||||
this.setState({deleting: true});
|
console.log("Delete widget %s", this.props.id);
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
this.setState({deleting: true});
|
||||||
this.props.room.roomId,
|
MatrixClientPeg.get().sendStateEvent(
|
||||||
'im.vector.modular.widgets',
|
this.props.room.roomId,
|
||||||
{}, // empty content
|
'im.vector.modular.widgets',
|
||||||
this.props.id,
|
{}, // empty content
|
||||||
).then(() => {
|
this.props.id,
|
||||||
console.log('Deleted widget');
|
).then(() => {
|
||||||
}, (e) => {
|
console.log('Deleted widget');
|
||||||
console.error('Failed to delete widget', e);
|
}, (e) => {
|
||||||
this.setState({deleting: false});
|
console.error('Failed to delete widget', e);
|
||||||
});
|
this.setState({deleting: false});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Revoke widget permissions - %s", this.props.id);
|
||||||
|
this._revokeWidgetPermission();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Widget labels to render, depending upon user permissions
|
||||||
|
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||||
|
_deleteWidgetLabel() {
|
||||||
|
if (this._canUserModify()) {
|
||||||
|
return 'Delete widget';
|
||||||
|
}
|
||||||
|
return 'Revoke widget access';
|
||||||
|
},
|
||||||
|
|
||||||
|
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||||
|
_grantWidgetPermission() {
|
||||||
|
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
|
||||||
|
localStorage.setItem(this.state.widgetPermissionId, true);
|
||||||
|
this.setState({hasPermissionToLoad: true});
|
||||||
|
},
|
||||||
|
|
||||||
|
_revokeWidgetPermission() {
|
||||||
|
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
||||||
|
localStorage.removeItem(this.state.widgetPermissionId);
|
||||||
|
this.setState({hasPermissionToLoad: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
formatAppTileName: function() {
|
formatAppTileName: function() {
|
||||||
|
@ -133,34 +190,66 @@ export default React.createClass({
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
||||||
|
// because that would allow the iframe to prgramatically remove the sandbox attribute, but
|
||||||
|
// this would only be for content hosted on the same origin as the riot client: anything
|
||||||
|
// hosted on the same origin as the client will get the same access as if you clicked
|
||||||
|
// a link to it.
|
||||||
|
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||||
|
"allow-same-origin allow-scripts allow-presentation";
|
||||||
|
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||||
|
let safeWidgetUrl = '';
|
||||||
|
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||||
|
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div> Loading... </div>
|
<div className='mx_AppTileBody mx_AppLoading'>
|
||||||
|
<MessageSpinner msg='Loading...'/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else if (this.state.hasPermissionToLoad == true) {
|
||||||
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
if (this.isMixedContent()) {
|
||||||
// because that would allow the iframe to prgramatically remove the sandbox attribute, but
|
appTileBody = (
|
||||||
// this would only be for content hosted on the same origin as the riot client: anything
|
<div className="mx_AppTileBody">
|
||||||
// hosted on the same origin as the client will get the same access as if you clicked
|
<AppWarning
|
||||||
// a link to it.
|
errorMsg="Error - Mixed content"
|
||||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
/>
|
||||||
"allow-same-origin allow-scripts";
|
</div>
|
||||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
);
|
||||||
let safeWidgetUrl = '';
|
} else {
|
||||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
appTileBody = (
|
||||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
<div className="mx_AppTileBody">
|
||||||
|
<iframe
|
||||||
|
ref="appFrame"
|
||||||
|
src={safeWidgetUrl}
|
||||||
|
allowFullScreen="true"
|
||||||
|
sandbox={sandboxFlags}
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className="mx_AppTileBody">
|
<div className="mx_AppTileBody">
|
||||||
<iframe ref="appFrame" src={safeWidgetUrl} allowFullScreen="true"
|
<AppPermission
|
||||||
sandbox={sandboxFlags}
|
url={this.state.widgetUrl}
|
||||||
></iframe>
|
onPermissionGranted={this._grantWidgetPermission}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// editing is done in scalar
|
// editing is done in scalar
|
||||||
const showEditButton = Boolean(this._scalarClient);
|
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
|
||||||
|
const deleteWidgetLabel = this._deleteWidgetLabel();
|
||||||
|
let deleteIcon = 'img/cancel.svg';
|
||||||
|
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
|
||||||
|
if(this._canUserModify()) {
|
||||||
|
deleteIcon = 'img/cancel-red.svg';
|
||||||
|
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||||
|
@ -172,14 +261,18 @@ export default React.createClass({
|
||||||
{showEditButton && <img
|
{showEditButton && <img
|
||||||
src="img/edit.svg"
|
src="img/edit.svg"
|
||||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
width="8" height="8" alt="Edit"
|
width="8" height="8"
|
||||||
|
alt={_t('Edit')}
|
||||||
|
title={_t('Edit')}
|
||||||
onClick={this._onEditClick}
|
onClick={this._onEditClick}
|
||||||
/>}
|
/>}
|
||||||
|
|
||||||
{/* Delete widget */}
|
{/* Delete widget */}
|
||||||
<img src="img/cancel.svg"
|
<img src={deleteIcon}
|
||||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget"
|
className={deleteClasses}
|
||||||
width="8" height="8" alt={_t("Cancel")}
|
width="8" height="8"
|
||||||
|
alt={_t(deleteWidgetLabel)}
|
||||||
|
title={_t(deleteWidgetLabel)}
|
||||||
onClick={this._onDeleteClick}
|
onClick={this._onDeleteClick}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
25
src/components/views/elements/AppWarning.js
Normal file
25
src/components/views/elements/AppWarning.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
const AppWarning = (props) => {
|
||||||
|
return (
|
||||||
|
<div className='mx_AppPermissionWarning'>
|
||||||
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
|
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||||
|
</div>
|
||||||
|
<div className='mx_AppPermissionWarningText'>
|
||||||
|
<span className='mx_AppPermissionWarningTextLabel'>{props.errorMsg}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AppWarning.propTypes = {
|
||||||
|
errorMsg: PropTypes.string,
|
||||||
|
};
|
||||||
|
AppWarning.defaultProps = {
|
||||||
|
errorMsg: 'Error',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppWarning;
|
|
@ -52,7 +52,7 @@ export default React.createClass({
|
||||||
|
|
||||||
onVerifyClick: function() {
|
onVerifyClick: function() {
|
||||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||||
Modal.createDialog(DeviceVerifyDialog, {
|
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||||
userId: this.props.userId,
|
userId: this.props.userId,
|
||||||
device: this.state.device,
|
device: this.state.device,
|
||||||
});
|
});
|
||||||
|
|
34
src/components/views/elements/MessageSpinner.js
Normal file
34
src/components/views/elements/MessageSpinner.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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';
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MessageSpinner',
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const w = this.props.w || 32;
|
||||||
|
const h = this.props.h || 32;
|
||||||
|
const imgClass = this.props.imgClassName || "";
|
||||||
|
const msg = this.props.msg || "Loading...";
|
||||||
|
return (
|
||||||
|
<div className="mx_Spinner">
|
||||||
|
<div className="mx_Spinner_Msg">{msg}</div>
|
||||||
|
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -25,8 +25,8 @@ import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||||
|
|
||||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||||
// HttpUtils transformTags to relative links
|
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+]).*)$/;
|
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+])[^\/]*)$/;
|
||||||
|
|
||||||
const Pill = React.createClass({
|
const Pill = React.createClass({
|
||||||
statics: {
|
statics: {
|
||||||
|
@ -47,6 +47,8 @@ const Pill = React.createClass({
|
||||||
inMessage: PropTypes.bool,
|
inMessage: PropTypes.bool,
|
||||||
// The room in which this pill is being rendered
|
// The room in which this pill is being rendered
|
||||||
room: PropTypes.instanceOf(Room),
|
room: PropTypes.instanceOf(Room),
|
||||||
|
// Whether to include an avatar in the pill
|
||||||
|
shouldShowPillAvatar: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
|
@ -63,16 +65,15 @@ const Pill = React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillReceiveProps(nextProps) {
|
||||||
this._unmounted = false;
|
|
||||||
let regex = REGEX_MATRIXTO;
|
let regex = REGEX_MATRIXTO;
|
||||||
if (this.props.inMessage) {
|
if (nextProps.inMessage) {
|
||||||
regex = REGEX_LOCAL_MATRIXTO;
|
regex = REGEX_LOCAL_MATRIXTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to the empty array if no match for simplicity
|
// Default to the empty array if no match for simplicity
|
||||||
// resource and prefix will be undefined instead of throwing
|
// resource and prefix will be undefined instead of throwing
|
||||||
const matrixToMatch = regex.exec(this.props.url) || [];
|
const matrixToMatch = regex.exec(nextProps.url) || [];
|
||||||
|
|
||||||
const resourceId = matrixToMatch[1]; // The room/user ID
|
const resourceId = matrixToMatch[1]; // The room/user ID
|
||||||
const prefix = matrixToMatch[2]; // The first character of prefix
|
const prefix = matrixToMatch[2]; // The first character of prefix
|
||||||
|
@ -87,7 +88,7 @@ const Pill = React.createClass({
|
||||||
let room;
|
let room;
|
||||||
switch (pillType) {
|
switch (pillType) {
|
||||||
case Pill.TYPE_USER_MENTION: {
|
case Pill.TYPE_USER_MENTION: {
|
||||||
const localMember = this.props.room.getMember(resourceId);
|
const localMember = nextProps.room.getMember(resourceId);
|
||||||
member = localMember;
|
member = localMember;
|
||||||
if (!localMember) {
|
if (!localMember) {
|
||||||
member = new RoomMember(null, resourceId);
|
member = new RoomMember(null, resourceId);
|
||||||
|
@ -112,6 +113,11 @@ const Pill = React.createClass({
|
||||||
this.setState({resourceId, pillType, member, room});
|
this.setState({resourceId, pillType, member, room});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this._unmounted = false;
|
||||||
|
this.componentWillReceiveProps(this.props);
|
||||||
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._unmounted = true;
|
this._unmounted = true;
|
||||||
},
|
},
|
||||||
|
@ -150,8 +156,10 @@ const Pill = React.createClass({
|
||||||
const member = this.state.member;
|
const member = this.state.member;
|
||||||
if (member) {
|
if (member) {
|
||||||
userId = member.userId;
|
userId = member.userId;
|
||||||
linkText = member.name;
|
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
|
||||||
avatar = <MemberAvatar member={member} width={16} height={16}/>;
|
if (this.props.shouldShowPillAvatar) {
|
||||||
|
avatar = <MemberAvatar member={member} width={16} height={16}/>;
|
||||||
|
}
|
||||||
pillClass = 'mx_UserPill';
|
pillClass = 'mx_UserPill';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,7 +168,9 @@ const Pill = React.createClass({
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (room) {
|
if (room) {
|
||||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||||
avatar = <RoomAvatar room={room} width={16} height={16}/>;
|
if (this.props.shouldShowPillAvatar) {
|
||||||
|
avatar = <RoomAvatar room={room} width={16} height={16}/>;
|
||||||
|
}
|
||||||
pillClass = 'mx_RoomPill';
|
pillClass = 'mx_RoomPill';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ module.exports = React.createClass({
|
||||||
if (this.allFieldsValid()) {
|
if (this.allFieldsValid()) {
|
||||||
if (this.refs.email.value == '') {
|
if (this.refs.email.value == '') {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -122,7 +122,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
showHelpPopup: function() {
|
showHelpPopup: function() {
|
||||||
var CustomServerDialog = sdk.getComponent('login.CustomServerDialog');
|
var CustomServerDialog = sdk.getComponent('login.CustomServerDialog');
|
||||||
Modal.createDialog(CustomServerDialog);
|
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
|
|
@ -282,8 +282,8 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.warn("Unable to decrypt attachment: ", err);
|
console.warn("Unable to decrypt attachment: ", err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Error decrypting attachment"),
|
description: _t("Error decrypting attachment"),
|
||||||
});
|
});
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
|
@ -170,6 +170,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
pillifyLinks: function(nodes) {
|
pillifyLinks: function(nodes) {
|
||||||
|
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
const node = nodes[i];
|
const node = nodes[i];
|
||||||
if (node.tagName === "A" && node.getAttribute("href")) {
|
if (node.tagName === "A" && node.getAttribute("href")) {
|
||||||
|
@ -181,7 +182,12 @@ module.exports = React.createClass({
|
||||||
const pillContainer = document.createElement('span');
|
const pillContainer = document.createElement('span');
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||||
const pill = <Pill url={href} inMessage={true} room={room}/>;
|
const pill = <Pill
|
||||||
|
url={href}
|
||||||
|
inMessage={true}
|
||||||
|
room={room}
|
||||||
|
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||||
|
/>;
|
||||||
|
|
||||||
ReactDOM.render(pill, pillContainer);
|
ReactDOM.render(pill, pillContainer);
|
||||||
node.parentNode.replaceChild(pillContainer, node);
|
node.parentNode.replaceChild(pillContainer, node);
|
||||||
|
@ -269,18 +275,21 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getEventTileOps: function() {
|
getEventTileOps: function() {
|
||||||
var self = this;
|
|
||||||
return {
|
return {
|
||||||
isWidgetHidden: function() {
|
isWidgetHidden: () => {
|
||||||
return self.state.widgetHidden;
|
return this.state.widgetHidden;
|
||||||
},
|
},
|
||||||
|
|
||||||
unhideWidget: function() {
|
unhideWidget: () => {
|
||||||
self.setState({ widgetHidden: false });
|
this.setState({ widgetHidden: false });
|
||||||
if (global.localStorage) {
|
if (global.localStorage) {
|
||||||
global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId());
|
global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInnerText: () => {
|
||||||
|
return this.refs.content.innerText;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -299,7 +308,7 @@ module.exports = React.createClass({
|
||||||
let completeUrl = scalarClient.getStarterLink(starterLink);
|
let completeUrl = scalarClient.getStarterLink(starterLink);
|
||||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
let integrationsUrl = SdkConfig.get().integrations_ui_url;
|
let integrationsUrl = SdkConfig.get().integrations_ui_url;
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
|
||||||
title: _t("Add an Integration"),
|
title: _t("Add an Integration"),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -154,7 +154,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
|
||||||
title: _t('Invalid alias format'),
|
title: _t('Invalid alias format'),
|
||||||
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
||||||
});
|
});
|
||||||
|
@ -170,7 +170,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
|
||||||
title: _t('Invalid address format'),
|
title: _t('Invalid address format'),
|
||||||
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
|
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,6 +26,7 @@ import SdkConfig from '../../../SdkConfig';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import ScalarMessaging from '../../../ScalarMessaging';
|
import ScalarMessaging from '../../../ScalarMessaging';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import WidgetUtils from '../../../WidgetUtils';
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -147,6 +148,15 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_canUserModify: function() {
|
||||||
|
try {
|
||||||
|
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onClickAddWidget: function(e) {
|
onClickAddWidget: function(e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -156,7 +166,7 @@ module.exports = React.createClass({
|
||||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
|
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
|
||||||
null;
|
null;
|
||||||
Modal.createDialog(IntegrationsManager, {
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
src: src,
|
src: src,
|
||||||
}, "mx_IntegrationsManager");
|
}, "mx_IntegrationsManager");
|
||||||
},
|
},
|
||||||
|
@ -164,7 +174,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const apps = this.state.apps.map(
|
const apps = this.state.apps.map(
|
||||||
(app, index, arr) => {
|
(app, index, arr) => {
|
||||||
return <AppTile
|
return (<AppTile
|
||||||
key={app.id}
|
key={app.id}
|
||||||
id={app.id}
|
id={app.id}
|
||||||
url={app.url}
|
url={app.url}
|
||||||
|
@ -173,10 +183,10 @@ module.exports = React.createClass({
|
||||||
fullWidth={arr.length<2 ? true : false}
|
fullWidth={arr.length<2 ? true : false}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
/>;
|
/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
const addWidget = this.state.apps && this.state.apps.length < 2 &&
|
const addWidget = this.state.apps && this.state.apps.length < 2 && this._canUserModify() &&
|
||||||
(<div onClick={this.onClickAddWidget}
|
(<div onClick={this.onClickAddWidget}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
|
|
|
@ -172,7 +172,7 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.setState({hide: true, selectionOffset: 0});
|
this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []});
|
||||||
}
|
}
|
||||||
|
|
||||||
forceComplete() {
|
forceComplete() {
|
||||||
|
|
|
@ -155,7 +155,9 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
if (nextProps.mxEvent !== this.props.mxEvent) {
|
// re-check the sender verification as outgoing events progress through
|
||||||
|
// the send process.
|
||||||
|
if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
|
||||||
this._verifyEvent(nextProps.mxEvent);
|
this._verifyEvent(nextProps.mxEvent);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -367,7 +369,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
onCryptoClicked: function(e) {
|
onCryptoClicked: function(e) {
|
||||||
var event = this.props.mxEvent;
|
var event = this.props.mxEvent;
|
||||||
|
|
||||||
Modal.createDialogAsync((cb) => {
|
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', (cb) => {
|
||||||
require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb);
|
require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb);
|
||||||
}, {
|
}, {
|
||||||
event: event,
|
event: event,
|
||||||
|
@ -386,6 +388,36 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderE2EPadlock: function() {
|
||||||
|
const ev = this.props.mxEvent;
|
||||||
|
const props = {onClick: this.onCryptoClicked};
|
||||||
|
|
||||||
|
|
||||||
|
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||||
|
return <E2ePadlockUndecryptable {...props}/>;
|
||||||
|
} else if (ev.isEncrypted()) {
|
||||||
|
if (this.state.verified) {
|
||||||
|
return <E2ePadlockVerified {...props}/>;
|
||||||
|
} else {
|
||||||
|
return <E2ePadlockUnverified {...props}/>;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// XXX: if the event is being encrypted (ie eventSendStatus ===
|
||||||
|
// encrypting), it might be nice to show something other than the
|
||||||
|
// open padlock?
|
||||||
|
|
||||||
|
// if the event is not encrypted, but it's an e2e room, show the
|
||||||
|
// open padlock
|
||||||
|
const e2eEnabled = this.props.matrixClient.isRoomEncrypted(ev.getRoomId());
|
||||||
|
if (e2eEnabled) {
|
||||||
|
return <E2ePadlockUnencrypted {...props}/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no padlock needed
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
||||||
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||||
|
@ -407,7 +439,6 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
throw new Error("Event type not supported");
|
throw new Error("Event type not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId());
|
|
||||||
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||||
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
||||||
|
|
||||||
|
@ -485,26 +516,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const editButton = (
|
const editButton = (
|
||||||
<span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} />
|
<span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} />
|
||||||
);
|
);
|
||||||
let e2e;
|
|
||||||
// cosmetic padlocks:
|
|
||||||
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
|
|
||||||
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12" />;
|
|
||||||
}
|
|
||||||
// real padlocks
|
|
||||||
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
|
|
||||||
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
|
|
||||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Undecryptable")} src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
|
|
||||||
}
|
|
||||||
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
|
|
||||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12"/>;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by an unverified device")} src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (e2eEnabled) {
|
|
||||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Unencrypted message")} src="img/e2e-unencrypted.svg" width="12" height="12"/>;
|
|
||||||
}
|
|
||||||
const timestamp = this.props.mxEvent.getTs() ?
|
const timestamp = this.props.mxEvent.getTs() ?
|
||||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||||
|
|
||||||
|
@ -572,7 +584,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
{ e2e }
|
{ this._renderE2EPadlock() }
|
||||||
<EventTileType ref="tile"
|
<EventTileType ref="tile"
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
highlights={this.props.highlights}
|
highlights={this.props.highlights}
|
||||||
|
@ -597,3 +609,39 @@ module.exports.haveTileForEvent = function(e) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function E2ePadlockUndecryptable(props) {
|
||||||
|
return (
|
||||||
|
<E2ePadlock alt={_t("Undecryptable")}
|
||||||
|
src="img/e2e-blocked.svg" width="12" height="12"
|
||||||
|
style={{ marginLeft: "-1px" }} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function E2ePadlockVerified(props) {
|
||||||
|
return (
|
||||||
|
<E2ePadlock alt={_t("Encrypted by a verified device")}
|
||||||
|
src="img/e2e-verified.svg" width="10" height="12"
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function E2ePadlockUnverified(props) {
|
||||||
|
return (
|
||||||
|
<E2ePadlock alt={_t("Encrypted by an unverified device")}
|
||||||
|
src="img/e2e-warning.svg" width="15" height="12"
|
||||||
|
style={{ marginLeft: "-2px" }} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function E2ePadlockUnencrypted(props) {
|
||||||
|
return (
|
||||||
|
<E2ePadlock alt={_t("Unencrypted message")}
|
||||||
|
src="img/e2e-unencrypted.svg" width="12" height="12"
|
||||||
|
{...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function E2ePadlock(props) {
|
||||||
|
return <img className="mx_EventTile_e2eIcon" {...props} />;
|
||||||
|
}
|
||||||
|
|
|
@ -229,7 +229,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const membership = this.props.member.membership;
|
const membership = this.props.member.membership;
|
||||||
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
|
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
|
||||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||||
Modal.createDialog(ConfirmUserActionDialog, {
|
Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, {
|
||||||
member: this.props.member,
|
member: this.props.member,
|
||||||
action: kickLabel,
|
action: kickLabel,
|
||||||
askReason: membership == "join",
|
askReason: membership == "join",
|
||||||
|
@ -248,7 +248,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Kick error: " + err);
|
console.error("Kick error: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, {
|
||||||
title: _t("Failed to kick"),
|
title: _t("Failed to kick"),
|
||||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||||
});
|
});
|
||||||
|
@ -262,7 +262,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
|
|
||||||
onBanOrUnban: function() {
|
onBanOrUnban: function() {
|
||||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||||
Modal.createDialog(ConfirmUserActionDialog, {
|
Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, {
|
||||||
member: this.props.member,
|
member: this.props.member,
|
||||||
action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"),
|
action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"),
|
||||||
askReason: this.props.member.membership != 'ban',
|
askReason: this.props.member.membership != 'ban',
|
||||||
|
@ -290,7 +290,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Ban error: " + err);
|
console.error("Ban error: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Failed to ban user"),
|
description: _t("Failed to ban user"),
|
||||||
});
|
});
|
||||||
|
@ -340,7 +340,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
console.log("Mute toggle success");
|
console.log("Mute toggle success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Mute error: " + err);
|
console.error("Mute error: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Failed to mute user"),
|
description: _t("Failed to mute user"),
|
||||||
});
|
});
|
||||||
|
@ -385,7 +385,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
dis.dispatch({action: 'view_set_mxid'});
|
dis.dispatch({action: 'view_set_mxid'});
|
||||||
} else {
|
} else {
|
||||||
console.error("Toggle moderator error:" + err);
|
console.error("Toggle moderator error:" + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to toggle moderator status', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Failed to toggle moderator status"),
|
description: _t("Failed to toggle moderator status"),
|
||||||
});
|
});
|
||||||
|
@ -406,7 +406,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to change power level " + err);
|
console.error("Failed to change power level " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Failed to change power level"),
|
description: _t("Failed to change power level"),
|
||||||
});
|
});
|
||||||
|
@ -435,7 +435,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
var myPower = powerLevelEvent.getContent().users[this.props.matrixClient.credentials.userId];
|
var myPower = powerLevelEvent.getContent().users[this.props.matrixClient.credentials.userId];
|
||||||
if (parseInt(myPower) === parseInt(powerLevel)) {
|
if (parseInt(myPower) === parseInt(powerLevel)) {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations 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.
|
||||||
|
@ -200,11 +201,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_createOverflowTile: function(overflowCount, totalCount) {
|
_createOverflowTile: function(overflowCount, totalCount) {
|
||||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||||
var EntityTile = sdk.getComponent("rooms.EntityTile");
|
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
var text = (overflowCount > 1)
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
? _t("and %(overflowCount)s others...", { overflowCount: overflowCount })
|
|
||||||
: _t("and one other...");
|
|
||||||
return (
|
return (
|
||||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
||||||
|
|
|
@ -99,7 +99,7 @@ export default class MessageComposer extends React.Component {
|
||||||
</li>);
|
</li>);
|
||||||
}
|
}
|
||||||
|
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
|
||||||
title: _t('Upload Files'),
|
title: _t('Upload Files'),
|
||||||
description: (
|
description: (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -31,6 +31,7 @@ import KeyCode from '../../../KeyCode';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import Analytics from '../../../Analytics';
|
||||||
|
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
@ -50,7 +51,7 @@ const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g')
|
||||||
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(' + asciiRegexp + ')\\s$');
|
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||||
|
|
||||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
@ -97,20 +98,39 @@ export default class MessageComposerInput extends React.Component {
|
||||||
onInputStateChanged: React.PropTypes.func,
|
onInputStateChanged: React.PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
|
||||||
// C-m => Toggles between rich text and markdown modes
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||||
if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
let ctrlCmdOnly;
|
||||||
return 'toggle-mode';
|
if (isMac) {
|
||||||
|
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
||||||
|
} else {
|
||||||
|
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I
|
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
|
||||||
if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) {
|
// importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
|
||||||
// When null is returned, draft-js will NOT preventDefault, allowing dev tools
|
// handle this in `getDefaultKeyBinding` so we do it ourselves here.
|
||||||
// to be toggled when the editor is focussed
|
//
|
||||||
return null;
|
// * if macOS, read second option
|
||||||
|
const ctrlCmdCommand = {
|
||||||
|
// C-m => Toggles between rich text and markdown modes
|
||||||
|
[KeyCode.KEY_M]: 'toggle-mode',
|
||||||
|
[KeyCode.KEY_B]: 'bold',
|
||||||
|
[KeyCode.KEY_I]: 'italic',
|
||||||
|
[KeyCode.KEY_U]: 'underline',
|
||||||
|
[KeyCode.KEY_J]: 'code',
|
||||||
|
[KeyCode.KEY_O]: 'split-block',
|
||||||
|
}[ev.keyCode];
|
||||||
|
|
||||||
|
if (ctrlCmdCommand) {
|
||||||
|
if (!ctrlCmdOnly) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ctrlCmdCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDefaultKeyBinding(e);
|
// Handle keys such as return, left and right arrows etc.
|
||||||
|
return getDefaultKeyBinding(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getBlockStyle(block: ContentBlock): ?string {
|
static getBlockStyle(block: ContentBlock): ?string {
|
||||||
|
@ -141,6 +161,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
||||||
|
|
||||||
|
Analytics.setRichtextMode(isRichtextEnabled);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// whether we're in rich text or markdown mode
|
// whether we're in rich text or markdown mode
|
||||||
isRichtextEnabled,
|
isRichtextEnabled,
|
||||||
|
@ -165,17 +187,18 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.client = MatrixClientPeg.get();
|
this.client = MatrixClientPeg.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
findLinkEntities(contentBlock, callback) {
|
findLinkEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
|
||||||
contentBlock.findEntityRanges(
|
contentBlock.findEntityRanges(
|
||||||
(character) => {
|
(character) => {
|
||||||
const entityKey = character.getEntity();
|
const entityKey = character.getEntity();
|
||||||
return (
|
return (
|
||||||
entityKey !== null &&
|
entityKey !== null &&
|
||||||
Entity.get(entityKey).getType() === 'LINK'
|
contentState.getEntity(entityKey).getType() === 'LINK'
|
||||||
);
|
);
|
||||||
}, callback,
|
}, callback,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* "Does the right thing" to create an EditorState, based on:
|
* "Does the right thing" to create an EditorState, based on:
|
||||||
* - whether we've got rich text mode enabled
|
* - whether we've got rich text mode enabled
|
||||||
|
@ -184,13 +207,19 @@ export default class MessageComposerInput extends React.Component {
|
||||||
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
|
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
|
||||||
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
|
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
|
||||||
RichText.getScopedMDDecorators(this.props);
|
RichText.getScopedMDDecorators(this.props);
|
||||||
|
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
||||||
decorators.push({
|
decorators.push({
|
||||||
strategy: this.findLinkEntities.bind(this),
|
strategy: this.findLinkEntities.bind(this),
|
||||||
component: (entityProps) => {
|
component: (entityProps) => {
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
const {url} = Entity.get(entityProps.entityKey).getData();
|
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
|
||||||
if (Pill.isPillUrl(url)) {
|
if (Pill.isPillUrl(url)) {
|
||||||
return <Pill url={url} room={this.props.room} offsetKey={entityProps.offsetKey}/>;
|
return <Pill
|
||||||
|
url={url}
|
||||||
|
room={this.props.room}
|
||||||
|
offsetKey={entityProps.offsetKey}
|
||||||
|
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -243,7 +272,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
// paths for inserting a user pill is not fun
|
// paths for inserting a user pill is not fun
|
||||||
const selection = this.state.editorState.getSelection();
|
const selection = this.state.editorState.getSelection();
|
||||||
const member = this.props.room.getMember(payload.user_id);
|
const member = this.props.room.getMember(payload.user_id);
|
||||||
const completion = member ? member.name.replace(' (IRC)', '') : payload.user_id;
|
const completion = member ?
|
||||||
|
member.rawDisplayName.replace(' (IRC)', '') : payload.user_id;
|
||||||
this.setDisplayedCompletion({
|
this.setDisplayedCompletion({
|
||||||
completion,
|
completion,
|
||||||
selection,
|
selection,
|
||||||
|
@ -253,10 +283,12 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'quote': {
|
case 'quote': {
|
||||||
let {body, formatted_body} = payload.event.getContent();
|
/// XXX: Not doing rich-text quoting from formatted-body because draft-js
|
||||||
formatted_body = formatted_body || escape(body);
|
/// has regressed such that when links are quoted, errors are thrown. See
|
||||||
if (formatted_body) {
|
/// https://github.com/vector-im/riot-web/issues/4756.
|
||||||
let content = RichText.htmlToContentState(`<blockquote>${formatted_body}</blockquote>`);
|
let body = escape(payload.text);
|
||||||
|
if (body) {
|
||||||
|
let content = RichText.htmlToContentState(`<blockquote>${body}</blockquote>`);
|
||||||
if (!this.state.isRichtextEnabled) {
|
if (!this.state.isRichtextEnabled) {
|
||||||
content = ContentState.createFromText(RichText.stateToMarkdown(content));
|
content = ContentState.createFromText(RichText.stateToMarkdown(content));
|
||||||
}
|
}
|
||||||
|
@ -393,7 +425,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const newContentState = Modifier.replaceText(
|
const newContentState = Modifier.replaceText(
|
||||||
editorState.getCurrentContent(),
|
editorState.getCurrentContent(),
|
||||||
currentSelection.merge({
|
currentSelection.merge({
|
||||||
anchorOffset: currentStartOffset - emojiMatch[0].length,
|
anchorOffset: currentStartOffset - emojiMatch[1].length - 1,
|
||||||
focusOffset: currentStartOffset,
|
focusOffset: currentStartOffset,
|
||||||
}),
|
}),
|
||||||
unicodeEmoji,
|
unicodeEmoji,
|
||||||
|
@ -427,6 +459,19 @@ export default class MessageComposerInput extends React.Component {
|
||||||
state.editorState = RichText.attachImmutableEntitiesToEmoji(
|
state.editorState = RichText.attachImmutableEntitiesToEmoji(
|
||||||
state.editorState);
|
state.editorState);
|
||||||
|
|
||||||
|
// Hide the autocomplete if the cursor location changes but the plaintext
|
||||||
|
// content stays the same. We don't hide if the pt has changed because the
|
||||||
|
// autocomplete will probably have different completions to show.
|
||||||
|
if (
|
||||||
|
!state.editorState.getSelection().equals(
|
||||||
|
this.state.editorState.getSelection()
|
||||||
|
)
|
||||||
|
&& state.editorState.getCurrentContent().getPlainText() ===
|
||||||
|
this.state.editorState.getCurrentContent().getPlainText()
|
||||||
|
) {
|
||||||
|
this.autocomplete.hide();
|
||||||
|
}
|
||||||
|
|
||||||
if (state.editorState.getCurrentContent().hasText()) {
|
if (state.editorState.getCurrentContent().hasText()) {
|
||||||
this.onTypingActivity();
|
this.onTypingActivity();
|
||||||
} else {
|
} else {
|
||||||
|
@ -451,14 +496,20 @@ export default class MessageComposerInput extends React.Component {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const textContent = this.state.editorState.getCurrentContent().getPlainText();
|
||||||
|
const selection = RichText.selectionStateToTextOffsets(
|
||||||
|
this.state.editorState.getSelection(),
|
||||||
|
this.state.editorState.getCurrentContent().getBlocksAsArray());
|
||||||
if (this.props.onContentChanged) {
|
if (this.props.onContentChanged) {
|
||||||
const textContent = this.state.editorState
|
|
||||||
.getCurrentContent().getPlainText();
|
|
||||||
const selection = RichText.selectionStateToTextOffsets(
|
|
||||||
this.state.editorState.getSelection(),
|
|
||||||
this.state.editorState.getCurrentContent().getBlocksAsArray());
|
|
||||||
this.props.onContentChanged(textContent, selection);
|
this.props.onContentChanged(textContent, selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scroll to the bottom of the editor if the cursor is on the last line of the
|
||||||
|
// composer. For some reason the editor won't scroll automatically if we paste
|
||||||
|
// blocks of text in or insert newlines.
|
||||||
|
if (textContent.slice(selection.start).indexOf("\n") === -1) {
|
||||||
|
this.refs.editor.refs.editor.scrollTop = this.refs.editor.refs.editor.scrollHeight;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,6 +528,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
contentState = ContentState.createFromText(markdown);
|
contentState = ContentState.createFromText(markdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Analytics.setRichtextMode(enabled);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState(enabled, contentState),
|
editorState: this.createEditorState(enabled, contentState),
|
||||||
isRichtextEnabled: enabled,
|
isRichtextEnabled: enabled,
|
||||||
|
@ -496,14 +549,21 @@ export default class MessageComposerInput extends React.Component {
|
||||||
// These are block types, not handled by RichUtils by default.
|
// These are block types, not handled by RichUtils by default.
|
||||||
const blockCommands = ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'];
|
const blockCommands = ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'];
|
||||||
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
|
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
|
||||||
|
|
||||||
|
const shouldToggleBlockFormat = (
|
||||||
|
command === 'backspace' ||
|
||||||
|
command === 'split-block'
|
||||||
|
) && currentBlockType !== 'unstyled';
|
||||||
|
|
||||||
if (blockCommands.includes(command)) {
|
if (blockCommands.includes(command)) {
|
||||||
newState = RichUtils.toggleBlockType(this.state.editorState, command);
|
newState = RichUtils.toggleBlockType(this.state.editorState, command);
|
||||||
} else if (command === 'strike') {
|
} else if (command === 'strike') {
|
||||||
// this is the only inline style not handled by Draft by default
|
// this is the only inline style not handled by Draft by default
|
||||||
newState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH');
|
newState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH');
|
||||||
} else if (command === 'backspace' && currentBlockType !== 'unstyled') {
|
} else if (shouldToggleBlockFormat) {
|
||||||
const currentStartOffset = this.state.editorState.getSelection().getStartOffset();
|
const currentStartOffset = this.state.editorState.getSelection().getStartOffset();
|
||||||
if (currentStartOffset === 0) {
|
const currentEndOffset = this.state.editorState.getSelection().getEndOffset();
|
||||||
|
if (currentStartOffset === 0 && currentEndOffset === 0) {
|
||||||
// Toggle current block type (setting it to 'unstyled')
|
// Toggle current block type (setting it to 'unstyled')
|
||||||
newState = RichUtils.toggleBlockType(this.state.editorState, currentBlockType);
|
newState = RichUtils.toggleBlockType(this.state.editorState, currentBlockType);
|
||||||
}
|
}
|
||||||
|
@ -638,7 +698,17 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
let contentText = contentState.getPlainText(), contentHTML;
|
let contentText = contentState.getPlainText(), contentHTML;
|
||||||
|
|
||||||
const cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
// Strip MD user (tab-completed) mentions to preserve plaintext mention behaviour.
|
||||||
|
// We have to do this now as opposed to after calculating the contentText for MD
|
||||||
|
// mode because entity positions may not be maintained when using
|
||||||
|
// md.toPlaintext().
|
||||||
|
// Unfortunately this means we lose mentions in history when in MD mode. This
|
||||||
|
// would be fixed if history was stored as contentState.
|
||||||
|
contentText = this.removeMDLinks(contentState, ['@']);
|
||||||
|
|
||||||
|
// Some commands (/join) require pills to be replaced with their text content
|
||||||
|
const commandText = this.removeMDLinks(contentState, ['#']);
|
||||||
|
const cmd = SlashCommands.processInput(this.props.room.roomId, commandText);
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (!cmd.error) {
|
if (!cmd.error) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -651,7 +721,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Command failure: %s", err);
|
console.error("Command failure: %s", err);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Server error', '', ErrorDialog, {
|
||||||
title: _t("Server error"),
|
title: _t("Server error"),
|
||||||
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
|
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
|
||||||
});
|
});
|
||||||
|
@ -659,7 +729,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
} else if (cmd.error) {
|
} else if (cmd.error) {
|
||||||
console.error(cmd.error);
|
console.error(cmd.error);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
// TODO possibly track which command they ran (not its Arguments) here
|
||||||
|
Modal.createTrackedDialog('Command error', '', ErrorDialog, {
|
||||||
title: _t("Command error"),
|
title: _t("Command error"),
|
||||||
description: cmd.error,
|
description: cmd.error,
|
||||||
});
|
});
|
||||||
|
@ -691,7 +762,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const hasLink = blocks.some((block) => {
|
const hasLink = blocks.some((block) => {
|
||||||
return block.getCharacterList().filter((c) => {
|
return block.getCharacterList().filter((c) => {
|
||||||
const entityKey = c.getEntity();
|
const entityKey = c.getEntity();
|
||||||
return entityKey && Entity.get(entityKey).getType() === 'LINK';
|
return entityKey && contentState.getEntity(entityKey).getType() === 'LINK';
|
||||||
}).size > 0;
|
}).size > 0;
|
||||||
});
|
});
|
||||||
shouldSendHTML = hasLink;
|
shouldSendHTML = hasLink;
|
||||||
|
@ -702,7 +773,31 @@ export default class MessageComposerInput extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const md = new Markdown(contentText);
|
// Use the original contentState because `contentText` has had mentions
|
||||||
|
// stripped and these need to end up in contentHTML.
|
||||||
|
|
||||||
|
// Replace all Entities of type `LINK` with markdown link equivalents.
|
||||||
|
// TODO: move this into `Markdown` and do the same conversion in the other
|
||||||
|
// two places (toggling from MD->RT mode and loading MD history into RT mode)
|
||||||
|
// but this can only be done when history includes Entities.
|
||||||
|
const pt = contentState.getBlocksAsArray().map((block) => {
|
||||||
|
let blockText = block.getText();
|
||||||
|
let offset = 0;
|
||||||
|
this.findLinkEntities(contentState, block, (start, end) => {
|
||||||
|
const entity = contentState.getEntity(block.getEntityAt(start));
|
||||||
|
if (entity.getType() !== 'LINK') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = blockText.slice(offset + start, offset + end);
|
||||||
|
const url = entity.getData().url;
|
||||||
|
const mdLink = `[${text}](${url})`;
|
||||||
|
blockText = blockText.slice(0, offset + start) + mdLink + blockText.slice(offset + end);
|
||||||
|
offset += mdLink.length - text.length;
|
||||||
|
});
|
||||||
|
return blockText;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
const md = new Markdown(pt);
|
||||||
if (md.isPlainText()) {
|
if (md.isPlainText()) {
|
||||||
contentText = md.toPlaintext();
|
contentText = md.toPlaintext();
|
||||||
} else {
|
} else {
|
||||||
|
@ -726,35 +821,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
sendTextFn = this.client.sendEmoteMessage;
|
sendTextFn = this.client.sendEmoteMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip MD user (tab-completed) mentions to preserve plaintext mention behaviour
|
|
||||||
contentText = contentText.replace(REGEX_MATRIXTO_MARKDOWN_GLOBAL,
|
|
||||||
(markdownLink, text, resource, prefix, offset) => {
|
|
||||||
// Calculate the offset relative to the current block that the offset is in
|
|
||||||
let sum = 0;
|
|
||||||
const blocks = contentState.getBlocksAsArray();
|
|
||||||
let block;
|
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
|
||||||
block = blocks[i];
|
|
||||||
sum += block.getLength();
|
|
||||||
if (sum > offset) {
|
|
||||||
sum -= block.getLength();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
offset -= sum;
|
|
||||||
|
|
||||||
const entityKey = block.getEntityAt(offset);
|
|
||||||
const entity = entityKey ? Entity.get(entityKey) : null;
|
|
||||||
if (entity && entity.getData().isCompletion && prefix === '@') {
|
|
||||||
// This is a completed mention, so do not insert MD link, just text
|
|
||||||
return text;
|
|
||||||
} else {
|
|
||||||
// This is either a MD link that was typed into the composer or another
|
|
||||||
// type of pill (e.g. room pill)
|
|
||||||
return markdownLink;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let sendMessagePromise;
|
let sendMessagePromise;
|
||||||
if (contentHTML) {
|
if (contentHTML) {
|
||||||
sendMessagePromise = sendHtmlFn.call(
|
sendMessagePromise = sendHtmlFn.call(
|
||||||
|
@ -819,6 +885,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.moveAutocompleteSelection(up);
|
this.moveAutocompleteSelection(up);
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -914,35 +981,27 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const {range = null, completion = '', href = null, suffix = ''} = displayedCompletion;
|
const {range = null, completion = '', href = null, suffix = ''} = displayedCompletion;
|
||||||
|
let contentState = activeEditorState.getCurrentContent();
|
||||||
|
|
||||||
let entityKey;
|
let entityKey;
|
||||||
let mdCompletion;
|
|
||||||
if (href) {
|
if (href) {
|
||||||
entityKey = Entity.create('LINK', 'IMMUTABLE', {
|
contentState = contentState.createEntity('LINK', 'IMMUTABLE', {
|
||||||
url: href,
|
url: href,
|
||||||
isCompletion: true,
|
isCompletion: true,
|
||||||
});
|
});
|
||||||
if (!this.state.isRichtextEnabled) {
|
entityKey = contentState.getLastCreatedEntityKey();
|
||||||
mdCompletion = `[${completion}](${href})`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let selection;
|
let selection;
|
||||||
if (range) {
|
if (range) {
|
||||||
selection = RichText.textOffsetsToSelectionState(
|
selection = RichText.textOffsetsToSelectionState(
|
||||||
range, activeEditorState.getCurrentContent().getBlocksAsArray(),
|
range, contentState.getBlocksAsArray(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
selection = activeEditorState.getSelection();
|
selection = activeEditorState.getSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentState = Modifier.replaceText(
|
contentState = Modifier.replaceText(contentState, selection, completion, null, entityKey);
|
||||||
activeEditorState.getCurrentContent(),
|
|
||||||
selection,
|
|
||||||
mdCompletion || completion,
|
|
||||||
null,
|
|
||||||
entityKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Move the selection to the end of the block
|
// Move the selection to the end of the block
|
||||||
const afterSelection = contentState.getSelectionAfter();
|
const afterSelection = contentState.getSelectionAfter();
|
||||||
|
@ -1002,6 +1061,44 @@ export default class MessageComposerInput extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAutocompleteQuery(contentState: ContentState) {
|
||||||
|
// Don't send markdown links to the autocompleter
|
||||||
|
return this.removeMDLinks(contentState, ['@', '#']);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMDLinks(contentState: ContentState, prefixes: string[]) {
|
||||||
|
const plaintext = contentState.getPlainText();
|
||||||
|
if (!plaintext) return '';
|
||||||
|
return plaintext.replace(REGEX_MATRIXTO_MARKDOWN_GLOBAL,
|
||||||
|
(markdownLink, text, resource, prefix, offset) => {
|
||||||
|
if (!prefixes.includes(prefix)) return markdownLink;
|
||||||
|
// Calculate the offset relative to the current block that the offset is in
|
||||||
|
let sum = 0;
|
||||||
|
const blocks = contentState.getBlocksAsArray();
|
||||||
|
let block;
|
||||||
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
|
block = blocks[i];
|
||||||
|
sum += block.getLength();
|
||||||
|
if (sum > offset) {
|
||||||
|
sum -= block.getLength();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset -= sum;
|
||||||
|
|
||||||
|
const entityKey = block.getEntityAt(offset);
|
||||||
|
const entity = entityKey ? contentState.getEntity(entityKey) : null;
|
||||||
|
if (entity && entity.getData().isCompletion) {
|
||||||
|
// This is a completed mention, so do not insert MD link, just text
|
||||||
|
return text;
|
||||||
|
} else {
|
||||||
|
// This is either a MD link that was typed into the composer or another
|
||||||
|
// type of pill (e.g. room pill)
|
||||||
|
return markdownLink;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMarkdownToggleClicked = (e) => {
|
onMarkdownToggleClicked = (e) => {
|
||||||
e.preventDefault(); // don't steal focus from the editor!
|
e.preventDefault(); // don't steal focus from the editor!
|
||||||
this.handleKeyCommand('toggle-mode');
|
this.handleKeyCommand('toggle-mode');
|
||||||
|
@ -1027,7 +1124,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = activeEditorState.getCurrentContent();
|
const content = activeEditorState.getCurrentContent();
|
||||||
const contentText = content.getPlainText();
|
|
||||||
const selection = RichText.selectionStateToTextOffsets(activeEditorState.getSelection(),
|
const selection = RichText.selectionStateToTextOffsets(activeEditorState.getSelection(),
|
||||||
activeEditorState.getCurrentContent().getBlocksAsArray());
|
activeEditorState.getCurrentContent().getBlocksAsArray());
|
||||||
|
|
||||||
|
@ -1037,7 +1133,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
ref={(e) => this.autocomplete = e}
|
ref={(e) => this.autocomplete = e}
|
||||||
onConfirm={this.setDisplayedCompletion}
|
onConfirm={this.setDisplayedCompletion}
|
||||||
query={contentText}
|
query={this.getAutocompleteQuery(content)}
|
||||||
selection={selection}/>
|
selection={selection}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
|
|
|
@ -119,7 +119,7 @@ module.exports = React.createClass({
|
||||||
const errMsg = (typeof err === "string") ? err : (err.error || "");
|
const errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to set avatar: " + errMsg);
|
console.error("Failed to set avatar: " + errMsg);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Failed to set avatar."),
|
description: _t("Failed to set avatar."),
|
||||||
});
|
});
|
||||||
|
|
|
@ -46,7 +46,7 @@ const BannedUser = React.createClass({
|
||||||
|
|
||||||
_onUnbanClick: function() {
|
_onUnbanClick: function() {
|
||||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||||
Modal.createDialog(ConfirmUserActionDialog, {
|
Modal.createTrackedDialog('Confirm User Action Dialog', 'onUnbanClick', ConfirmUserActionDialog, {
|
||||||
member: this.props.member,
|
member: this.props.member,
|
||||||
action: _t('Unban'),
|
action: _t('Unban'),
|
||||||
danger: false,
|
danger: false,
|
||||||
|
@ -58,7 +58,7 @@ const BannedUser = React.createClass({
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to unban: " + err);
|
console.error("Failed to unban: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to unban', '', ErrorDialog, {
|
||||||
title: _t('Error'),
|
title: _t('Error'),
|
||||||
description: _t('Failed to unban'),
|
description: _t('Failed to unban'),
|
||||||
});
|
});
|
||||||
|
@ -423,7 +423,7 @@ module.exports = React.createClass({
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
var value = ev.target.value;
|
var value = ev.target.value;
|
||||||
|
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Privacy warning', '', QuestionDialog, {
|
||||||
title: _t('Privacy warning'),
|
title: _t('Privacy warning'),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
@ -516,7 +516,7 @@ module.exports = React.createClass({
|
||||||
onManageIntegrations(ev) {
|
onManageIntegrations(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
Modal.createDialog(IntegrationsManager, {
|
Modal.createTrackedDialog('Integrations Manager', 'onManageIntegrations', IntegrationsManager, {
|
||||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
||||||
null,
|
null,
|
||||||
|
@ -549,7 +549,7 @@ module.exports = React.createClass({
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
var errCode = err.errcode || _t('unknown error code');
|
var errCode = err.errcode || _t('unknown error code');
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
|
||||||
title: _t('Error'),
|
title: _t('Error'),
|
||||||
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
|
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
|
||||||
});
|
});
|
||||||
|
@ -560,7 +560,7 @@ module.exports = React.createClass({
|
||||||
if (!this.refs.encrypt.checked) return;
|
if (!this.refs.encrypt.checked) return;
|
||||||
|
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('E2E Enable Warning', '', QuestionDialog, {
|
||||||
title: _t('Warning!'),
|
title: _t('Warning!'),
|
||||||
description: (
|
description: (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -117,9 +117,7 @@ var SearchableEntityList = React.createClass({
|
||||||
_createOverflowEntity: function(overflowCount, totalCount) {
|
_createOverflowEntity: function(overflowCount, totalCount) {
|
||||||
var EntityTile = sdk.getComponent("rooms.EntityTile");
|
var EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
var text = (overflowCount > 1)
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
? _t("and %(overflowCount)s others...", { overflowCount: overflowCount })
|
|
||||||
: _t("and one other...");
|
|
||||||
return (
|
return (
|
||||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
||||||
|
|
|
@ -82,7 +82,7 @@ export default withMatrixClient(React.createClass({
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error("Unable to add phone number: " + err);
|
console.error("Unable to add phone number: " + err);
|
||||||
let msg = err.message;
|
let msg = err.message;
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Add Phone Number Error', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: msg,
|
description: msg,
|
||||||
});
|
});
|
||||||
|
@ -107,7 +107,7 @@ export default withMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
msgElements.push(<div key="_error" className="error">{msg}</div>);
|
msgElements.push(<div key="_error" className="error">{msg}</div>);
|
||||||
}
|
}
|
||||||
Modal.createDialog(TextInputDialog, {
|
Modal.createTrackedDialog('Prompt for MSISDN Verification Code', '', TextInputDialog, {
|
||||||
title: _t("Enter Code"),
|
title: _t("Enter Code"),
|
||||||
description: <div>{msgElements}</div>,
|
description: <div>{msgElements}</div>,
|
||||||
button: _t("Submit"),
|
button: _t("Submit"),
|
||||||
|
|
|
@ -104,7 +104,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
@ -164,7 +164,7 @@ module.exports = React.createClass({
|
||||||
const deferred = Promise.defer();
|
const deferred = Promise.defer();
|
||||||
// Ask for an email otherwise the user has no way to reset their password
|
// Ask for an email otherwise the user has no way to reset their password
|
||||||
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
|
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
|
||||||
Modal.createDialog(SetEmailDialog, {
|
Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
|
||||||
title: _t('Do you want to set an email address?'),
|
title: _t('Do you want to set an email address?'),
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
// ignore confirmed, setting an email is optional
|
// ignore confirmed, setting an email is optional
|
||||||
|
@ -175,15 +175,13 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onExportE2eKeysClicked: function() {
|
_onExportE2eKeysClicked: function() {
|
||||||
Modal.createDialogAsync(
|
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', (cb) => {
|
||||||
(cb) => {
|
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||||
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||||
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
}, "e2e-export");
|
||||||
}, "e2e-export");
|
}, {
|
||||||
}, {
|
matrixClient: MatrixClientPeg.get(),
|
||||||
matrixClient: MatrixClientPeg.get(),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onClickChange: function() {
|
onClickChange: function() {
|
||||||
|
|
|
@ -71,8 +71,8 @@ export default class DevicesPanelEntry extends React.Component {
|
||||||
// pop up an interactive auth dialog
|
// pop up an interactive auth dialog
|
||||||
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||||
|
|
||||||
Modal.createDialog(InteractiveAuthDialog, {
|
Modal.createTrackedDialog('Delete Device Dialog', InteractiveAuthDialog, {
|
||||||
title: _t("Authentication"),
|
title: _t("Authentication"),
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
authData: error.data,
|
authData: error.data,
|
||||||
makeRequest: this._makeDeleteRequest,
|
makeRequest: this._makeDeleteRequest,
|
||||||
|
|
|
@ -115,7 +115,7 @@ function createRoom(opts) {
|
||||||
action: 'join_room_error',
|
action: 'join_room_error',
|
||||||
});
|
});
|
||||||
console.error("Failed to create room " + roomId + " " + err);
|
console.error("Failed to create room " + roomId + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failure to create room', '', ErrorDialog, {
|
||||||
title: _t("Failure to create room"),
|
title: _t("Failure to create room"),
|
||||||
description: _t("Server may be unavailable, overloaded, or you hit a bug."),
|
description: _t("Server may be unavailable, overloaded, or you hit a bug."),
|
||||||
});
|
});
|
||||||
|
|
|
@ -552,8 +552,10 @@
|
||||||
"Failed to forget room %(errCode)s": "Das Entfernen des Raums ist fehlgeschlagen %(errCode)s",
|
"Failed to forget room %(errCode)s": "Das Entfernen des Raums ist fehlgeschlagen %(errCode)s",
|
||||||
"Failed to join the room": "Fehler beim Betreten des Raumes",
|
"Failed to join the room": "Fehler beim Betreten des Raumes",
|
||||||
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Eine Textnachricht wurde an +%(msisdn)s gesendet. Bitte gebe den Verifikationscode ein, den er beinhaltet",
|
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Eine Textnachricht wurde an +%(msisdn)s gesendet. Bitte gebe den Verifikationscode ein, den er beinhaltet",
|
||||||
"and %(overflowCount)s others...": "und %(overflowCount)s weitere...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "und ein(e) weitere(r)...",
|
"other": "und %(count)s weitere...",
|
||||||
|
"one": "und ein(e) weitere(r)..."
|
||||||
|
},
|
||||||
"Are you sure?": "Bist du sicher?",
|
"Are you sure?": "Bist du sicher?",
|
||||||
"Attachment": "Anhang",
|
"Attachment": "Anhang",
|
||||||
"Ban": "Dauerhaft aus dem Raum ausschließen",
|
"Ban": "Dauerhaft aus dem Raum ausschließen",
|
||||||
|
|
|
@ -175,8 +175,10 @@
|
||||||
"an address": "μία διεύθηνση",
|
"an address": "μία διεύθηνση",
|
||||||
"%(items)s and %(remaining)s others": "%(items)s και %(remaining)s ακόμα",
|
"%(items)s and %(remaining)s others": "%(items)s και %(remaining)s ακόμα",
|
||||||
"%(items)s and one other": "%(items)s και ένας ακόμα",
|
"%(items)s and one other": "%(items)s και ένας ακόμα",
|
||||||
"and %(overflowCount)s others...": "και %(overflowCount)s άλλοι...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "και ένας ακόμα...",
|
"other": "και %(count)s άλλοι...",
|
||||||
|
"one": "και ένας ακόμα..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s και %(lastPerson)s γράφουν",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s και %(lastPerson)s γράφουν",
|
||||||
"%(names)s and one other are typing": "%(names)s και ένας ακόμα γράφουν",
|
"%(names)s and one other are typing": "%(names)s και ένας ακόμα γράφουν",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s και %(count)s άλλοι γράφουν",
|
"%(names)s and %(count)s others are typing": "%(names)s και %(count)s άλλοι γράφουν",
|
||||||
|
|
|
@ -134,6 +134,7 @@
|
||||||
"Add phone number": "Add phone number",
|
"Add phone number": "Add phone number",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
"Admin tools": "Admin tools",
|
"Admin tools": "Admin tools",
|
||||||
|
"Allow": "Allow",
|
||||||
"And %(count)s more...": "And %(count)s more...",
|
"And %(count)s more...": "And %(count)s more...",
|
||||||
"VoIP": "VoIP",
|
"VoIP": "VoIP",
|
||||||
"Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.",
|
"Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.",
|
||||||
|
@ -157,8 +158,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s and %(remaining)s others",
|
"%(items)s and %(remaining)s others": "%(items)s and %(remaining)s others",
|
||||||
"%(items)s and one other": "%(items)s and one other",
|
"%(items)s and one other": "%(items)s and one other",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "and %(overflowCount)s others...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "and one other...",
|
"other": "and %(count)s others...",
|
||||||
|
"one": "and one other..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
|
||||||
"%(names)s and one other are typing": "%(names)s and one other are typing",
|
"%(names)s and one other are typing": "%(names)s and one other are typing",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s and %(count)s others are typing",
|
"%(names)s and %(count)s others are typing": "%(names)s and %(count)s others are typing",
|
||||||
|
@ -239,6 +242,7 @@
|
||||||
"Decrypt %(text)s": "Decrypt %(text)s",
|
"Decrypt %(text)s": "Decrypt %(text)s",
|
||||||
"Decryption error": "Decryption error",
|
"Decryption error": "Decryption error",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
|
"Delete widget": "Delete widget",
|
||||||
"demote": "demote",
|
"demote": "demote",
|
||||||
"Deops user with given id": "Deops user with given id",
|
"Deops user with given id": "Deops user with given id",
|
||||||
"Default": "Default",
|
"Default": "Default",
|
||||||
|
@ -265,6 +269,7 @@
|
||||||
"Drop here %(toAction)s": "Drop here %(toAction)s",
|
"Drop here %(toAction)s": "Drop here %(toAction)s",
|
||||||
"Drop here to tag %(section)s": "Drop here to tag %(section)s",
|
"Drop here to tag %(section)s": "Drop here to tag %(section)s",
|
||||||
"Ed25519 fingerprint": "Ed25519 fingerprint",
|
"Ed25519 fingerprint": "Ed25519 fingerprint",
|
||||||
|
"Edit": "Edit",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"Email address": "Email address",
|
"Email address": "Email address",
|
||||||
"Email address (optional)": "Email address (optional)",
|
"Email address (optional)": "Email address (optional)",
|
||||||
|
@ -340,6 +345,8 @@
|
||||||
"had": "had",
|
"had": "had",
|
||||||
"Hangup": "Hangup",
|
"Hangup": "Hangup",
|
||||||
"Hide Apps": "Hide Apps",
|
"Hide Apps": "Hide Apps",
|
||||||
|
"Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)",
|
||||||
|
"Hide avatar and display name changes": "Hide avatar and display name changes",
|
||||||
"Hide read receipts": "Hide read receipts",
|
"Hide read receipts": "Hide read receipts",
|
||||||
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
||||||
"Historical": "Historical",
|
"Historical": "Historical",
|
||||||
|
@ -457,6 +464,7 @@
|
||||||
"Reason": "Reason",
|
"Reason": "Reason",
|
||||||
"Reason: %(reasonText)s": "Reason: %(reasonText)s",
|
"Reason: %(reasonText)s": "Reason: %(reasonText)s",
|
||||||
"Revoke Moderator": "Revoke Moderator",
|
"Revoke Moderator": "Revoke Moderator",
|
||||||
|
"Revoke widget access": "Revoke widget access",
|
||||||
"Refer a friend to Riot:": "Refer a friend to Riot:",
|
"Refer a friend to Riot:": "Refer a friend to Riot:",
|
||||||
"Register": "Register",
|
"Register": "Register",
|
||||||
"rejected": "rejected",
|
"rejected": "rejected",
|
||||||
|
@ -568,6 +576,7 @@
|
||||||
"To configure the room": "To configure the room",
|
"To configure the room": "To configure the room",
|
||||||
"to demote": "to demote",
|
"to demote": "to demote",
|
||||||
"to favourite": "to favourite",
|
"to favourite": "to favourite",
|
||||||
|
"To get started, please pick a username!": "To get started, please pick a username!",
|
||||||
"To invite users into the room": "To invite users into the room",
|
"To invite users into the room": "To invite users into the room",
|
||||||
"To kick users": "To kick users",
|
"To kick users": "To kick users",
|
||||||
"To link to a room it must have <a>an address</a>.": "To link to a room it must have <a>an address</a>.",
|
"To link to a room it must have <a>an address</a>.": "To link to a room it must have <a>an address</a>.",
|
||||||
|
@ -959,5 +968,6 @@
|
||||||
"Edit Group": "Edit Group",
|
"Edit Group": "Edit Group",
|
||||||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||||
"Failed to upload image": "Failed to upload image",
|
"Failed to upload image": "Failed to upload image",
|
||||||
"Failed to update group": "Failed to update group"
|
"Failed to update group": "Failed to update group",
|
||||||
|
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions"
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,8 +154,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s and %(remaining)s others",
|
"%(items)s and %(remaining)s others": "%(items)s and %(remaining)s others",
|
||||||
"%(items)s and one other": "%(items)s and one other",
|
"%(items)s and one other": "%(items)s and one other",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "and %(overflowCount)s others...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "and one other...",
|
"other": "and %(count)s others...",
|
||||||
|
"one": "and one other..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
|
||||||
"%(names)s and one other are typing": "%(names)s and one other are typing",
|
"%(names)s and one other are typing": "%(names)s and one other are typing",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s and %(count)s others are typing",
|
"%(names)s and %(count)s others are typing": "%(names)s and %(count)s others are typing",
|
||||||
|
@ -231,6 +233,7 @@
|
||||||
"demote": "demote",
|
"demote": "demote",
|
||||||
"Deops user with given id": "Deops user with given id",
|
"Deops user with given id": "Deops user with given id",
|
||||||
"Default": "Default",
|
"Default": "Default",
|
||||||
|
"Delete widget": "Delete widget",
|
||||||
"Device already verified!": "Device already verified!",
|
"Device already verified!": "Device already verified!",
|
||||||
"Device ID": "Device ID",
|
"Device ID": "Device ID",
|
||||||
"Device ID:": "Device ID:",
|
"Device ID:": "Device ID:",
|
||||||
|
@ -250,6 +253,7 @@
|
||||||
"Drop here %(toAction)s": "Drop here %(toAction)s",
|
"Drop here %(toAction)s": "Drop here %(toAction)s",
|
||||||
"Drop here to tag %(section)s": "Drop here to tag %(section)s",
|
"Drop here to tag %(section)s": "Drop here to tag %(section)s",
|
||||||
"Ed25519 fingerprint": "Ed25519 fingerprint",
|
"Ed25519 fingerprint": "Ed25519 fingerprint",
|
||||||
|
"Edit": "Edit",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"Email address": "Email address",
|
"Email address": "Email address",
|
||||||
"Email address (optional)": "Email address (optional)",
|
"Email address (optional)": "Email address (optional)",
|
||||||
|
@ -419,6 +423,7 @@
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Reason": "Reason",
|
"Reason": "Reason",
|
||||||
"Revoke Moderator": "Revoke Moderator",
|
"Revoke Moderator": "Revoke Moderator",
|
||||||
|
"Revoke widget access": "Revoke widget access",
|
||||||
"Refer a friend to Riot:": "Refer a friend to Riot:",
|
"Refer a friend to Riot:": "Refer a friend to Riot:",
|
||||||
"Register": "Register",
|
"Register": "Register",
|
||||||
"rejected": "rejected",
|
"rejected": "rejected",
|
||||||
|
|
|
@ -140,8 +140,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s y %(remaining)s otros",
|
"%(items)s and %(remaining)s others": "%(items)s y %(remaining)s otros",
|
||||||
"%(items)s and one other": "%(items)s y otro",
|
"%(items)s and one other": "%(items)s y otro",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s y %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s y %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "y %(overflowCount)s otros...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "y otro...",
|
"other": "y %(count)s otros...",
|
||||||
|
"one": "y otro..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s y %(lastPerson)s están escribiendo",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s y %(lastPerson)s están escribiendo",
|
||||||
"%(names)s and one other are typing": "%(names)s y otro están escribiendo",
|
"%(names)s and one other are typing": "%(names)s y otro están escribiendo",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s y %(count)s otros están escribiendo",
|
"%(names)s and %(count)s others are typing": "%(names)s y %(count)s otros están escribiendo",
|
||||||
|
|
|
@ -188,8 +188,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s et %(remaining)s autres",
|
"%(items)s and %(remaining)s others": "%(items)s et %(remaining)s autres",
|
||||||
"%(items)s and one other": "%(items)s et un autre",
|
"%(items)s and one other": "%(items)s et un autre",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s et %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s et %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "et %(overflowCount)s autres...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "et un autre...",
|
"other": "et %(count)s autres...",
|
||||||
|
"one": "et un autre..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s et %(lastPerson)s sont en train de taper",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s et %(lastPerson)s sont en train de taper",
|
||||||
"%(names)s and one other are typing": "%(names)s et un autre sont en train de taper",
|
"%(names)s and one other are typing": "%(names)s et un autre sont en train de taper",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s et %(count)s d'autres sont en train de taper",
|
"%(names)s and %(count)s others are typing": "%(names)s et %(count)s d'autres sont en train de taper",
|
||||||
|
|
|
@ -190,8 +190,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s és még: %(remaining)s",
|
"%(items)s and %(remaining)s others": "%(items)s és még: %(remaining)s",
|
||||||
"%(items)s and one other": "%(items)s és még egy",
|
"%(items)s and one other": "%(items)s és még egy",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s és %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s és %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "és még: %(overflowCount)s ...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "és még egy...",
|
"other": "és még: %(count)s ...",
|
||||||
|
"one": "és még egy..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s és %(lastPerson)s írnak",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s és %(lastPerson)s írnak",
|
||||||
"%(names)s and one other are typing": "%(names)s és még valaki ír",
|
"%(names)s and one other are typing": "%(names)s és még valaki ír",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s és %(count)s ember ír",
|
"%(names)s and %(count)s others are typing": "%(names)s és %(count)s ember ír",
|
||||||
|
|
|
@ -225,8 +225,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s과 %(remaining)s",
|
"%(items)s and %(remaining)s others": "%(items)s과 %(remaining)s",
|
||||||
"%(items)s and one other": "%(items)s과 다른 하나",
|
"%(items)s and one other": "%(items)s과 다른 하나",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s과 %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s과 %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "그리고 %(overflowCount)s...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "그리고 다른 하나...",
|
"other": "그리고 %(count)s...",
|
||||||
|
"one": "그리고 다른 하나..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s님과 %(lastPerson)s님이 입력중",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s님과 %(lastPerson)s님이 입력중",
|
||||||
"%(names)s and one other are typing": "%(names)s님과 다른 분이 입력중",
|
"%(names)s and one other are typing": "%(names)s님과 다른 분이 입력중",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s님과 %(count)s 분들이 입력중",
|
"%(names)s and %(count)s others are typing": "%(names)s님과 %(count)s 분들이 입력중",
|
||||||
|
|
|
@ -141,8 +141,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s en %(remaining)s andere",
|
"%(items)s and %(remaining)s others": "%(items)s en %(remaining)s andere",
|
||||||
"%(items)s and one other": "%(items)s en één andere",
|
"%(items)s and one other": "%(items)s en één andere",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s en %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s en %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "en %(overflowCount)s andere...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "en één andere...",
|
"other": "en %(count)s andere...",
|
||||||
|
"one": "en één andere..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s en %(lastPerson)s zijn aan het typen",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s en %(lastPerson)s zijn aan het typen",
|
||||||
"%(names)s and one other are typing": "%(names)s en één andere zijn aan het typen",
|
"%(names)s and one other are typing": "%(names)s en één andere zijn aan het typen",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s en %(count)s andere zijn aan het typen",
|
"%(names)s and %(count)s others are typing": "%(names)s en %(count)s andere zijn aan het typen",
|
||||||
|
|
|
@ -556,8 +556,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s e %(remaining)s outros",
|
"%(items)s and %(remaining)s others": "%(items)s e %(remaining)s outros",
|
||||||
"%(items)s and one other": "%(items)s e um outro",
|
"%(items)s and one other": "%(items)s e um outro",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s e %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s e %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "e %(overflowCount)s outros...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "e um outro...",
|
"other": "e %(count)s outros...",
|
||||||
|
"one": "e um outro..."
|
||||||
|
},
|
||||||
"Are you sure?": "Você tem certeza?",
|
"Are you sure?": "Você tem certeza?",
|
||||||
"Attachment": "Anexo",
|
"Attachment": "Anexo",
|
||||||
"Autoplay GIFs and videos": "Reproduzir automaticamente GIFs e videos",
|
"Autoplay GIFs and videos": "Reproduzir automaticamente GIFs e videos",
|
||||||
|
|
|
@ -557,8 +557,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s e %(remaining)s outros",
|
"%(items)s and %(remaining)s others": "%(items)s e %(remaining)s outros",
|
||||||
"%(items)s and one other": "%(items)s e um outro",
|
"%(items)s and one other": "%(items)s e um outro",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s e %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s e %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "e %(overflowCount)s outros...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "e um outro...",
|
"other": "e %(count)s outros...",
|
||||||
|
"one": "e um outro..."
|
||||||
|
},
|
||||||
"Are you sure?": "Você tem certeza?",
|
"Are you sure?": "Você tem certeza?",
|
||||||
"Attachment": "Anexo",
|
"Attachment": "Anexo",
|
||||||
"Autoplay GIFs and videos": "Reproduzir automaticamente GIFs e videos",
|
"Autoplay GIFs and videos": "Reproduzir automaticamente GIFs e videos",
|
||||||
|
|
|
@ -448,7 +448,10 @@
|
||||||
"sx": "Суту",
|
"sx": "Суту",
|
||||||
"zh-hk": "Китайский (Гонконг)",
|
"zh-hk": "Китайский (Гонконг)",
|
||||||
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "На +%(msisdn)s было отправлено текстовое сообщение. Пожалуйста, введите проверочный код из него",
|
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "На +%(msisdn)s было отправлено текстовое сообщение. Пожалуйста, введите проверочный код из него",
|
||||||
"and %(overflowCount)s others...": "и %(overflowCount)s других...",
|
"and %(count)s others...": {
|
||||||
|
"other": "и %(count)s других...",
|
||||||
|
"one": "и ещё один..."
|
||||||
|
},
|
||||||
"Are you sure?": "Вы уверены?",
|
"Are you sure?": "Вы уверены?",
|
||||||
"Autoplay GIFs and videos": "Проигрывать GIF и видео автоматически",
|
"Autoplay GIFs and videos": "Проигрывать GIF и видео автоматически",
|
||||||
"Can't connect to homeserver - please check your connectivity and ensure your <a>homeserver's SSL certificate</a> is trusted.": "Невозможно соединиться с домашним сервером - проверьте своё соединение и убедитесь, что <a>SSL-сертификат вашего домашнего сервера</a> включён в доверяемые.",
|
"Can't connect to homeserver - please check your connectivity and ensure your <a>homeserver's SSL certificate</a> is trusted.": "Невозможно соединиться с домашним сервером - проверьте своё соединение и убедитесь, что <a>SSL-сертификат вашего домашнего сервера</a> включён в доверяемые.",
|
||||||
|
@ -479,7 +482,6 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s и другие %(remaining)s",
|
"%(items)s and %(remaining)s others": "%(items)s и другие %(remaining)s",
|
||||||
"%(items)s and one other": "%(items)s и ещё один",
|
"%(items)s and one other": "%(items)s и ещё один",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s и %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s и %(lastItem)s",
|
||||||
"and one other...": "и ещё один...",
|
|
||||||
"An error has occurred.": "Произошла ошибка.",
|
"An error has occurred.": "Произошла ошибка.",
|
||||||
"Attachment": "Вложение",
|
"Attachment": "Вложение",
|
||||||
"Ban": "Запретить",
|
"Ban": "Запретить",
|
||||||
|
|
|
@ -150,8 +150,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s och %(remaining)s andra",
|
"%(items)s and %(remaining)s others": "%(items)s och %(remaining)s andra",
|
||||||
"%(items)s and one other": "%(items)s och en annan",
|
"%(items)s and one other": "%(items)s och en annan",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s och %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s och %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "och %(overflowCount)s andra...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "och en annan...",
|
"other": "och %(count)s andra...",
|
||||||
|
"one": "och en annan..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s och %(lastPerson)s skriver",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s och %(lastPerson)s skriver",
|
||||||
"%(names)s and one other are typing": "%(names)s och en annan skriver",
|
"%(names)s and one other are typing": "%(names)s och en annan skriver",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s och %(count)s andra skriver",
|
"%(names)s and %(count)s others are typing": "%(names)s och %(count)s andra skriver",
|
||||||
|
|
|
@ -98,8 +98,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s และอีก %(remaining)s ผู้ใช้",
|
"%(items)s and %(remaining)s others": "%(items)s และอีก %(remaining)s ผู้ใช้",
|
||||||
"%(items)s and one other": "%(items)s และอีกหนึ่งผู้ใช้",
|
"%(items)s and one other": "%(items)s และอีกหนึ่งผู้ใช้",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s และ %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s และ %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "และอีก %(overflowCount)s ผู้ใช้...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "และอีกหนึ่งผู้ใช้...",
|
"other": "และอีก %(count)s ผู้ใช้...",
|
||||||
|
"one": "และอีกหนึ่งผู้ใช้..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s และ %(lastPerson)s กำลังพิมพ์",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s และ %(lastPerson)s กำลังพิมพ์",
|
||||||
"%(names)s and one other are typing": "%(names)s และอีกหนึ่งคนกำลังพิมพ์",
|
"%(names)s and one other are typing": "%(names)s และอีกหนึ่งคนกำลังพิมพ์",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s และอีก %(count)s คนกำลังพิมพ์",
|
"%(names)s and %(count)s others are typing": "%(names)s และอีก %(count)s คนกำลังพิมพ์",
|
||||||
|
|
|
@ -156,8 +156,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s ve %(remaining)s diğerleri",
|
"%(items)s and %(remaining)s others": "%(items)s ve %(remaining)s diğerleri",
|
||||||
"%(items)s and one other": "%(items)s ve bir başkası",
|
"%(items)s and one other": "%(items)s ve bir başkası",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s ve %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s ve %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "ve %(overflowCount)s diğerleri...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "ve bir diğeri...",
|
"other": "ve %(count)s diğerleri...",
|
||||||
|
"one": "ve bir diğeri..."
|
||||||
|
},
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s ve %(lastPerson)s yazıyorlar",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s ve %(lastPerson)s yazıyorlar",
|
||||||
"%(names)s and one other are typing": "%(names)s ve birisi yazıyor",
|
"%(names)s and one other are typing": "%(names)s ve birisi yazıyor",
|
||||||
"%(names)s and %(count)s others are typing": "%(names)s ve %(count)s diğeri yazıyor",
|
"%(names)s and %(count)s others are typing": "%(names)s ve %(count)s diğeri yazıyor",
|
||||||
|
|
|
@ -239,8 +239,10 @@
|
||||||
"%(items)s and %(remaining)s others": "%(items)s 和其它 %(remaining)s 个",
|
"%(items)s and %(remaining)s others": "%(items)s 和其它 %(remaining)s 个",
|
||||||
"%(items)s and one other": "%(items)s 和其它一个",
|
"%(items)s and one other": "%(items)s 和其它一个",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s 和 %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s 和 %(lastItem)s",
|
||||||
"and %(overflowCount)s others...": "和其它 %(overflowCount)s 个...",
|
"and %(count)s others...": {
|
||||||
"and one other...": "和其它一个...",
|
"other": "和其它 %(count)s 个...",
|
||||||
|
"one": "和其它一个..."
|
||||||
|
},
|
||||||
"%(names)s and one other are typing": "%(names)s 和另一个人正在打字",
|
"%(names)s and one other are typing": "%(names)s 和另一个人正在打字",
|
||||||
"anyone": "任何人",
|
"anyone": "任何人",
|
||||||
"Anyone": "任何人",
|
"Anyone": "任何人",
|
||||||
|
|
50
src/shouldHideEvent.js
Normal file
50
src/shouldHideEvent.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function memberEventDiff(ev) {
|
||||||
|
const diff = {
|
||||||
|
isMemberEvent: ev.getType() === 'm.room.member',
|
||||||
|
};
|
||||||
|
|
||||||
|
// If is not a Member Event then the other checks do not apply, so bail early.
|
||||||
|
if (!diff.isMemberEvent) return diff;
|
||||||
|
|
||||||
|
const content = ev.getContent();
|
||||||
|
const prevContent = ev.getPrevContent();
|
||||||
|
|
||||||
|
diff.isJoin = content.membership === 'join' && prevContent.membership !== 'ban';
|
||||||
|
diff.isPart = content.membership === 'leave' && ev.getStateKey() === ev.getSender();
|
||||||
|
|
||||||
|
const isJoinToJoin = content.membership === prevContent.membership && content.membership === 'join';
|
||||||
|
diff.isDisplaynameChange = isJoinToJoin && content.displayname !== prevContent.displayname;
|
||||||
|
diff.isAvatarChange = isJoinToJoin && content.avatar_url !== prevContent.avatar_url;
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function shouldHideEvent(ev, syncedSettings) {
|
||||||
|
// Hide redacted events
|
||||||
|
if (syncedSettings['hideRedactions'] && ev.isRedacted()) return true;
|
||||||
|
|
||||||
|
const eventDiff = memberEventDiff(ev);
|
||||||
|
|
||||||
|
if (eventDiff.isMemberEvent) {
|
||||||
|
if (syncedSettings['hideJoinLeaves'] && (eventDiff.isJoin || eventDiff.isPart)) return true;
|
||||||
|
const isMemberAvatarDisplaynameChange = eventDiff.isAvatarChange || eventDiff.isDisplaynameChange;
|
||||||
|
if (syncedSettings['hideAvatarDisplaynameChanges'] && isMemberAvatarDisplaynameChange) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -221,7 +221,7 @@ class RoomViewStore extends Store {
|
||||||
});
|
});
|
||||||
const msg = err.message ? err.message : JSON.stringify(err);
|
const msg = err.message ? err.message : JSON.stringify(err);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
|
||||||
title: _t("Failed to join room"),
|
title: _t("Failed to join room"),
|
||||||
description: msg,
|
description: msg,
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,10 +18,12 @@ var React = require('react');
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var TestUtils = require('react-addons-test-utils');
|
var TestUtils = require('react-addons-test-utils');
|
||||||
var expect = require('expect');
|
var expect = require('expect');
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
var sdk = require('matrix-react-sdk');
|
var sdk = require('matrix-react-sdk');
|
||||||
|
|
||||||
var MessagePanel = sdk.getComponent('structures.MessagePanel');
|
var MessagePanel = sdk.getComponent('structures.MessagePanel');
|
||||||
|
import UserSettingsStore from '../../../src/UserSettingsStore';
|
||||||
|
|
||||||
var test_utils = require('test-utils');
|
var test_utils = require('test-utils');
|
||||||
var mockclock = require('mock-clock');
|
var mockclock = require('mock-clock');
|
||||||
|
@ -54,9 +56,10 @@ describe('MessagePanel', function () {
|
||||||
test_utils.beforeEach(this);
|
test_utils.beforeEach(this);
|
||||||
client = test_utils.createTestClient();
|
client = test_utils.createTestClient();
|
||||||
client.credentials = {userId: '@me:here'};
|
client.credentials = {userId: '@me:here'};
|
||||||
|
UserSettingsStore.getSyncedSettings = sinon.stub().returns({});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function() {
|
||||||
clock.uninstall();
|
clock.uninstall();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue