Merge remote-tracking branch 'origin/develop' into dbkr/udd_no_auto_show

This commit is contained in:
David Baker 2017-11-16 15:59:16 +00:00
commit 196eafdc7f
58 changed files with 645 additions and 336 deletions

View file

@ -29,6 +29,10 @@ module.exports = {
// so we replace it with a version that is class property aware // so we replace it with a version that is class property aware
"babel/no-invalid-this": "error", "babel/no-invalid-this": "error",
// We appear to follow this most of the time, so let's enforce it instead
// of occasionally following it (or catching it in review)
"keyword-spacing": "error",
/** react **/ /** react **/
// This just uses the react plugin to help eslint known when // This just uses the react plugin to help eslint known when
// variables have been used in JSX // variables have been used in JSX

View file

@ -32,7 +32,7 @@ const walk = require('walk');
const flowParser = require('flow-parser'); const flowParser = require('flow-parser');
const estreeWalker = require('estree-walker'); const estreeWalker = require('estree-walker');
const TRANSLATIONS_FUNCS = ['_t', '_td', '_tJsx']; const TRANSLATIONS_FUNCS = ['_t', '_td'];
const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; const OUTPUT_FILE = 'src/i18n/strings/en_EN.json';
@ -126,7 +126,7 @@ function getTranslationsJs(file) {
if (tKey === null) return; if (tKey === null) return;
// check the format string against the args // check the format string against the args
// We only check _t: _tJsx is much more complex and _td has no args // We only check _t: _td has no args
if (node.callee.name === '_t') { if (node.callee.name === '_t') {
try { try {
const placeholders = getFormatStrings(tKey); const placeholders = getFormatStrings(tKey);
@ -139,6 +139,22 @@ function getTranslationsJs(file) {
throw new Error(`No value found for placeholder '${placeholder}'`); throw new Error(`No value found for placeholder '${placeholder}'`);
} }
} }
// Validate tag replacements
if (node.arguments.length > 2) {
const tagMap = node.arguments[2];
for (const prop of tagMap.properties) {
if (prop.key.type === 'Literal') {
const tag = prop.key.value;
// RegExp same as in src/languageHandler.js
const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`);
if (!tKey.match(regexp)) {
throw new Error(`No match for ${regexp} in ${tKey}`);
}
}
}
}
} catch (e) { } catch (e) {
console.log(); console.log();
console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);

View file

@ -61,7 +61,7 @@ export default class ComposerHistoryManager {
// TODO: Performance issues? // TODO: Performance issues?
let item; let item;
for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
this.history.push( this.history.push(
Object.assign(new HistoryItem(), JSON.parse(item)), Object.assign(new HistoryItem(), JSON.parse(item)),
); );

View file

@ -84,7 +84,7 @@ class MatrixClientPeg {
if (this.matrixClient.initCrypto) { if (this.matrixClient.initCrypto) {
await this.matrixClient.initCrypto(); await this.matrixClient.initCrypto();
} }
} catch(e) { } catch (e) {
// this can happen for a number of reasons, the most likely being // this can happen for a number of reasons, the most likely being
// that the olm library was missing. It's not fatal. // that the olm library was missing. It's not fatal.
console.warn("Unable to initialise e2e: " + e); console.warn("Unable to initialise e2e: " + e);
@ -99,7 +99,7 @@ class MatrixClientPeg {
const promise = this.matrixClient.store.startup(); const promise = this.matrixClient.store.startup();
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`); console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
await promise; await promise;
} catch(err) { } catch (err) {
// log any errors when starting up the database (if one exists) // log any errors when starting up the database (if one exists)
console.error(`Error starting matrixclient store: ${err}`); console.error(`Error starting matrixclient store: ${err}`);
} }

View file

@ -68,7 +68,7 @@ function unicodeToEmojiUri(str) {
return unicodeChar; return unicodeChar;
} else { } else {
// Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below // Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below
if(unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') { if (unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') {
unicodeChar = unicodeChar[0]; unicodeChar = unicodeChar[0];
} }

View file

@ -151,9 +151,9 @@ function textForCallHangupEvent(event) {
const senderName = event.sender ? event.sender.name : _t('Someone'); const senderName = event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent(); const eventContent = event.getContent();
let reason = ""; let reason = "";
if(!MatrixClientPeg.get().supportsVoip()) { if (!MatrixClientPeg.get().supportsVoip()) {
reason = _t('(not supported by this browser)'); reason = _t('(not supported by this browser)');
} else if(eventContent.reason) { } else if (eventContent.reason) {
if (eventContent.reason === "ice_failed") { if (eventContent.reason === "ice_failed") {
reason = _t('(could not connect media)'); reason = _t('(could not connect media)');
} else if (eventContent.reason === "invite_timeout") { } else if (eventContent.reason === "invite_timeout") {

View file

@ -19,6 +19,10 @@ const DEBUG = 0;
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue] // utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
function colorToRgb(color) { function colorToRgb(color) {
if (!color) {
return [0, 0, 0];
}
if (color[0] === '#') { if (color[0] === '#') {
color = color.slice(1); color = color.slice(1);
if (color.length === 3) { if (color.length === 3) {
@ -31,16 +35,17 @@ function colorToRgb(color) {
const g = (val >> 8) & 255; const g = (val >> 8) & 255;
const b = val & 255; const b = val & 255;
return [r, g, b]; return [r, g, b];
} } else {
else { const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
let match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
if (match) { if (match) {
return [ parseInt(match[1]), return [
parseInt(match[2]), parseInt(match[1]),
parseInt(match[3]) ]; parseInt(match[2]),
parseInt(match[3]),
];
} }
} }
return [0,0,0]; return [0, 0, 0];
} }
// utility to turn [red,green,blue] into #rrggbb // utility to turn [red,green,blue] into #rrggbb
@ -72,6 +77,7 @@ class Tinter {
"#EAF5F0", // Vector Light Green "#EAF5F0", // Vector Light Green
"#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green) "#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme) "#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
"#000000", // black lowlights of the SVGs (for switching to dark theme)
]; ];
// track the replacement colours actually being used // track the replacement colours actually being used
@ -81,6 +87,7 @@ class Tinter {
this.keyHex[1], this.keyHex[1],
this.keyHex[2], this.keyHex[2],
this.keyHex[3], this.keyHex[3],
this.keyHex[4],
]; ];
// track the most current tint request inputs (which may differ from the // track the most current tint request inputs (which may differ from the
@ -90,6 +97,7 @@ class Tinter {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
]; ];
this.cssFixups = [ this.cssFixups = [
@ -152,9 +160,11 @@ class Tinter {
this.calcCssFixups(); this.calcCssFixups();
if (DEBUG) console.log("Tinter.tint(" + primaryColor + ", " + if (DEBUG) {
secondaryColor + ", " + console.log("Tinter.tint(" + primaryColor + ", " +
tertiaryColor + ")"); secondaryColor + ", " +
tertiaryColor + ")");
}
if (!primaryColor) { if (!primaryColor) {
primaryColor = this.keyRgb[0]; primaryColor = this.keyRgb[0];
@ -194,9 +204,11 @@ class Tinter {
this.colors[1] = secondaryColor; this.colors[1] = secondaryColor;
this.colors[2] = tertiaryColor; this.colors[2] = tertiaryColor;
if (DEBUG) console.log("Tinter.tint final: (" + primaryColor + ", " + if (DEBUG) {
secondaryColor + ", " + console.log("Tinter.tint final: (" + primaryColor + ", " +
tertiaryColor + ")"); secondaryColor + ", " +
tertiaryColor + ")");
}
// go through manually fixing up the stylesheets. // go through manually fixing up the stylesheets.
this.applyCssFixups(); this.applyCssFixups();
@ -223,25 +235,38 @@ class Tinter {
}); });
} }
setTheme(theme) { tintSvgBlack(blackColor) {
this.currentTint[4] = blackColor;
if (!blackColor) {
blackColor = this.colors[4];
}
if (this.colors[4] === blackColor) {
return;
}
this.colors[4] = blackColor;
this.tintables.forEach(function(tintable) {
tintable();
});
}
setTheme(theme) {
console.trace("setTheme " + theme); console.trace("setTheme " + theme);
this.theme = theme; this.theme = theme;
// update keyRgb from the current theme CSS itself, if it defines it // update keyRgb from the current theme CSS itself, if it defines it
if (document.getElementById('mx_theme_accentColor')) { if (document.getElementById('mx_theme_accentColor')) {
this.keyRgb[0] = window.getComputedStyle( this.keyRgb[0] = window.getComputedStyle(
document.getElementById('mx_theme_accentColor') document.getElementById('mx_theme_accentColor')).color;
).color;
} }
if (document.getElementById('mx_theme_secondaryAccentColor')) { if (document.getElementById('mx_theme_secondaryAccentColor')) {
this.keyRgb[1] = window.getComputedStyle( this.keyRgb[1] = window.getComputedStyle(
document.getElementById('mx_theme_secondaryAccentColor') document.getElementById('mx_theme_secondaryAccentColor')).color;
).color;
} }
if (document.getElementById('mx_theme_tertiaryAccentColor')) { if (document.getElementById('mx_theme_tertiaryAccentColor')) {
this.keyRgb[2] = window.getComputedStyle( this.keyRgb[2] = window.getComputedStyle(
document.getElementById('mx_theme_tertiaryAccentColor') document.getElementById('mx_theme_tertiaryAccentColor')).color;
).color;
} }
this.calcCssFixups(); this.calcCssFixups();
@ -253,8 +278,10 @@ class Tinter {
// abuse the tinter to change all the SVG's #fff to #2d2d2d // abuse the tinter to change all the SVG's #fff to #2d2d2d
// XXX: obviously this shouldn't be hardcoded here. // XXX: obviously this shouldn't be hardcoded here.
this.tintSvgWhite('#2d2d2d'); this.tintSvgWhite('#2d2d2d');
this.tintSvgBlack('#dddddd');
} else { } else {
this.tintSvgWhite('#ffffff'); this.tintSvgWhite('#ffffff');
this.tintSvgBlack('#000000');
} }
} }
@ -262,9 +289,11 @@ class Tinter {
// cache our fixups // cache our fixups
if (this.cssFixups[this.theme]) return; if (this.cssFixups[this.theme]) return;
if (DEBUG) console.debug("calcCssFixups start for " + this.theme + " (checking " + if (DEBUG) {
document.styleSheets.length + console.debug("calcCssFixups start for " + this.theme + " (checking " +
" stylesheets)"); document.styleSheets.length +
" stylesheets)");
}
this.cssFixups[this.theme] = []; this.cssFixups[this.theme] = [];
@ -322,21 +351,24 @@ class Tinter {
} }
} }
} }
if (DEBUG) console.log("calcCssFixups end (" + if (DEBUG) {
this.cssFixups[this.theme].length + console.log("calcCssFixups end (" +
" fixups)"); this.cssFixups[this.theme].length +
" fixups)");
}
} }
applyCssFixups() { applyCssFixups() {
if (DEBUG) console.log("applyCssFixups start (" + if (DEBUG) {
this.cssFixups[this.theme].length + console.log("applyCssFixups start (" +
" fixups)"); this.cssFixups[this.theme].length +
" fixups)");
}
for (let i = 0; i < this.cssFixups[this.theme].length; i++) { for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
const cssFixup = this.cssFixups[this.theme][i]; const cssFixup = this.cssFixups[this.theme][i];
try { try {
cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index]; cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
} } catch (e) {
catch (e) {
// Firefox Quantum explodes if you manually edit the CSS in the // Firefox Quantum explodes if you manually edit the CSS in the
// inspector and then try to do a tint, as apparently all the // inspector and then try to do a tint, as apparently all the
// fixups are then stale. // fixups are then stale.
@ -358,10 +390,10 @@ class Tinter {
if (DEBUG) console.log("calcSvgFixups start for " + svgs); if (DEBUG) console.log("calcSvgFixups start for " + svgs);
const fixups = []; const fixups = [];
for (let i = 0; i < svgs.length; i++) { for (let i = 0; i < svgs.length; i++) {
var svgDoc; let svgDoc;
try { try {
svgDoc = svgs[i].contentDocument; svgDoc = svgs[i].contentDocument;
} catch(e) { } catch (e) {
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString(); let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
if (e.message) { if (e.message) {
msg += e.message; msg += e.message;
@ -369,7 +401,7 @@ class Tinter {
if (e.stack) { if (e.stack) {
msg += ' | stack: ' + e.stack; msg += ' | stack: ' + e.stack;
} }
console.error(e); console.error(msg);
} }
if (!svgDoc) continue; if (!svgDoc) continue;
const tags = svgDoc.getElementsByTagName("*"); const tags = svgDoc.getElementsByTagName("*");
@ -379,8 +411,7 @@ class Tinter {
const attr = this.svgAttrs[k]; const attr = this.svgAttrs[k];
for (let l = 0; l < this.keyHex.length; l++) { for (let l = 0; l < this.keyHex.length; l++) {
if (tag.getAttribute(attr) && if (tag.getAttribute(attr) &&
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
{
fixups.push({ fixups.push({
node: tag, node: tag,
attr: attr, attr: attr,

View file

@ -126,7 +126,7 @@ export default class UserProvider extends AutocompleteProvider {
const events = this.room.getLiveTimeline().getEvents(); const events = this.room.getLiveTimeline().getEvents();
const lastSpoken = {}; const lastSpoken = {};
for(const event of events) { for (const event of events) {
lastSpoken[event.getSender()] = event.getTs(); lastSpoken[event.getSender()] = event.getTs();
} }

View file

@ -19,7 +19,7 @@ import React from 'react';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import sdk from '../../index'; import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import { _t, _tJsx } from '../../languageHandler'; import { _t } from '../../languageHandler';
/* /*
* Component which shows the filtered file using a TimelinePanel * Component which shows the filtered file using a TimelinePanel
@ -92,7 +92,10 @@ const FilePanel = React.createClass({
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper"> return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty"> <div className="mx_RoomView_empty">
{ _tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{ sub }</a>) } { _t("You must <a>register</a> to use this functionality",
{},
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
}
</div> </div>
</div>; </div>;
} else if (this.noRoom) { } else if (this.noRoom) {

View file

@ -22,7 +22,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import { sanitizedHtmlNode } from '../../HtmlUtils'; import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t, _td, _tJsx } from '../../languageHandler'; import { _t, _td } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal'; import Modal from '../../Modal';
import classnames from 'classnames'; import classnames from 'classnames';
@ -932,12 +932,12 @@ export default React.createClass({
className="mx_GroupView_groupDesc_placeholder" className="mx_GroupView_groupDesc_placeholder"
onClick={this._onEditClick} onClick={this._onEditClick}
> >
{ _tJsx( { _t(
'Your community hasn\'t got a Long Description, a HTML page to show to community members.<br />' + 'Your community hasn\'t got a Long Description, a HTML page to show to community members.<br />' +
'Click here to open settings and give it one!', 'Click here to open settings and give it one!',
[/<br \/>/], {},
[(sub) => <br />]) { 'br': <br /> },
} ) }
</div>; </div>;
} }
const groupDescEditingClasses = classnames({ const groupDescEditingClasses = classnames({

View file

@ -315,7 +315,7 @@ module.exports = React.createClass({
// the first thing to do is to try the token params in the query-string // the first thing to do is to try the token params in the query-string
Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => { Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => {
if(loggedIn) { if (loggedIn) {
this.props.onTokenLoginCompleted(); this.props.onTokenLoginCompleted();
// don't do anything else until the page reloads - just stay in // don't do anything else until the page reloads - just stay in
@ -888,7 +888,7 @@ module.exports = React.createClass({
*/ */
_onSetTheme: function(theme) { _onSetTheme: function(theme) {
if (!theme) { if (!theme) {
theme = this.props.config.default_theme || 'light'; theme = SettingsStore.getValueAt(SettingLevel.DEFAULT, "theme");
} }
// look for the stylesheet elements. // look for the stylesheet elements.

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import {MatrixClient} from 'matrix-js-sdk'; import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../index'; import sdk from '../../index';
import { _t, _tJsx } from '../../languageHandler'; import { _t } from '../../languageHandler';
import withMatrixClient from '../../wrappers/withMatrixClient'; import withMatrixClient from '../../wrappers/withMatrixClient';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
@ -165,13 +165,13 @@ export default withMatrixClient(React.createClass({
<div className="mx_MyGroups_headerCard_header"> <div className="mx_MyGroups_headerCard_header">
{ _t('Join an existing community') } { _t('Join an existing community') }
</div> </div>
{ _tJsx( { _t(
'To join an existing community you\'ll have to '+ 'To join an existing community you\'ll have to '+
'know its community identifier; this will look '+ 'know its community identifier; this will look '+
'something like <i>+example:matrix.org</i>.', 'something like <i>+example:matrix.org</i>.',
/<i>(.*)<\/i>/, {},
(sub) => <i>{ sub }</i>, { 'i': (sub) => <i>{ sub }</i> })
) } }
</div> </div>
</div> </div>
</div> </div>

View file

@ -16,8 +16,8 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { _t, _tJsx } from '../../languageHandler';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import { _t } from '../../languageHandler';
import sdk from '../../index'; import sdk from '../../index';
import WhoIsTyping from '../../WhoIsTyping'; import WhoIsTyping from '../../WhoIsTyping';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
@ -25,7 +25,6 @@ import MemberAvatar from '../views/avatars/MemberAvatar';
import Resend from '../../Resend'; import Resend from '../../Resend';
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices'; import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
const HIDE_DEBOUNCE_MS = 10000;
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
const STATUS_BAR_EXPANDED_LARGE = 2; const STATUS_BAR_EXPANDED_LARGE = 2;
@ -286,13 +285,13 @@ module.exports = React.createClass({
if (hasUDE) { if (hasUDE) {
title = _t("Message not sent due to unknown devices being present"); title = _t("Message not sent due to unknown devices being present");
content = _tJsx( content = _t(
"<a>Show devices</a> or <a>cancel all</a>.", "<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/], {},
[ {
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>, 'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>, 'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
], },
); );
} else { } else {
if ( if (
@ -305,14 +304,15 @@ module.exports = React.createClass({
} else { } else {
title = _t("Some of your messages have not been sent."); title = _t("Some of your messages have not been sent.");
} }
content = _tJsx( content = _t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
"<a>Resend all</a> or <a>cancel all</a> now. "+ "You can also select individual messages to resend or cancel.",
"You can also select individual messages to resend or cancel.", {},
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/], {
[ 'resendText': (sub) =>
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>, <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>, 'cancelText': (sub) =>
], <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
); );
} }
@ -391,12 +391,15 @@ module.exports = React.createClass({
if (this.props.sentMessageAndIsAlone) { if (this.props.sentMessageAndIsAlone) {
return ( return (
<div className="mx_RoomStatusBar_isAlone"> <div className="mx_RoomStatusBar_isAlone">
{ _tJsx("There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?", { _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/], "or <nowarnText>stop warning about the empty room</nowarnText>?",
[ {},
(sub) => <a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>, {
(sub) => <a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>, 'inviteText': (sub) =>
], <a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
'nowarnText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
},
) } ) }
</div> </div>
); );

View file

@ -299,7 +299,7 @@ module.exports = React.createClass({
// Check if user has previously chosen to hide the app drawer for this // Check if user has previously chosen to hide the app drawer for this
// room. If so, do not show apps // room. If so, do not show apps
let hideWidgetDrawer = localStorage.getItem( const hideWidgetDrawer = localStorage.getItem(
room.roomId + "_hide_widget_drawer"); room.roomId + "_hide_widget_drawer");
if (hideWidgetDrawer === "true") { if (hideWidgetDrawer === "true") {
@ -704,7 +704,7 @@ module.exports = React.createClass({
return; return;
} }
const joinedMembers = room.currentState.getMembers().filter(m => m.membership === "join" || m.membership === "invite"); const joinedMembers = room.currentState.getMembers().filter((m) => m.membership === "join" || m.membership === "invite");
this.setState({isAlone: joinedMembers.length === 1}); this.setState({isAlone: joinedMembers.length === 1});
}, },
@ -1060,7 +1060,7 @@ module.exports = React.createClass({
} }
if (this.state.searchScope === 'All') { if (this.state.searchScope === 'All') {
if(roomId != lastRoomId) { if (roomId != lastRoomId) {
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
// XXX: if we've left the room, we might not know about // XXX: if we've left the room, we might not know about
@ -1371,13 +1371,13 @@ module.exports = React.createClass({
*/ */
handleScrollKey: function(ev) { handleScrollKey: function(ev) {
let panel; let panel;
if(this.refs.searchResultsPanel) { if (this.refs.searchResultsPanel) {
panel = this.refs.searchResultsPanel; panel = this.refs.searchResultsPanel;
} else if(this.refs.messagePanel) { } else if (this.refs.messagePanel) {
panel = this.refs.messagePanel; panel = this.refs.messagePanel;
} }
if(panel) { if (panel) {
panel.handleScrollKey(ev); panel.handleScrollKey(ev);
} }
}, },
@ -1396,7 +1396,7 @@ module.exports = React.createClass({
// otherwise react calls it with null on each update. // otherwise react calls it with null on each update.
_gatherTimelinePanelRef: function(r) { _gatherTimelinePanelRef: function(r) {
this.refs.messagePanel = r; this.refs.messagePanel = r;
if(r) { if (r) {
console.log("updateTint from RoomView._gatherTimelinePanelRef"); console.log("updateTint from RoomView._gatherTimelinePanelRef");
this.updateTint(); this.updateTint();
} }

View file

@ -573,7 +573,7 @@ module.exports = React.createClass({
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" + debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")"); pixelOffset + " (delta: "+scrollDelta+")");
if(scrollDelta != 0) { if (scrollDelta != 0) {
this._setScrollTop(scrollNode.scrollTop + scrollDelta); this._setScrollTop(scrollNode.scrollTop + scrollDelta);
} }
}, },

View file

@ -310,7 +310,7 @@ var TimelinePanel = React.createClass({
return Promise.resolve(false); return Promise.resolve(false);
} }
if(!this._timelineWindow.canPaginate(dir)) { if (!this._timelineWindow.canPaginate(dir)) {
debuglog("TimelinePanel: can't", dir, "paginate any further"); debuglog("TimelinePanel: can't", dir, "paginate any further");
this.setState({[canPaginateKey]: false}); this.setState({[canPaginateKey]: false});
return Promise.resolve(false); return Promise.resolve(false);
@ -440,7 +440,7 @@ var TimelinePanel = React.createClass({
var callback = null; var callback = null;
if (sender != myUserId && !UserActivity.userCurrentlyActive()) { if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
updatedState.readMarkerVisible = true; updatedState.readMarkerVisible = true;
} else if(lastEv && this.getReadMarkerPosition() === 0) { } else if (lastEv && this.getReadMarkerPosition() === 0) {
// we know we're stuckAtBottom, so we can advance the RM // we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle // immediately, to save a later render cycle
@ -657,7 +657,7 @@ var TimelinePanel = React.createClass({
// the read-marker should become invisible, so that if the user scrolls // the read-marker should become invisible, so that if the user scrolls
// down, they don't see it. // down, they don't see it.
if(this.state.readMarkerVisible) { if (this.state.readMarkerVisible) {
this.setState({ this.setState({
readMarkerVisible: false, readMarkerVisible: false,
}); });

View file

@ -612,9 +612,8 @@ module.exports = React.createClass({
}, },
onLanguageChange: function(newLang) { onLanguageChange: function(newLang) {
if(this.state.language !== newLang) { if (this.state.language !== newLang) {
// We intentionally promote this to the account level at this point SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
SettingsStore.setValue("language", null, SettingLevel.ACCOUNT, newLang);
this.setState({ this.setState({
language: newLang, language: newLang,
}); });

View file

@ -154,7 +154,7 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const LoginPage = sdk.getComponent("login.LoginPage"); const LoginPage = sdk.getComponent("login.LoginPage");
const LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginHeader = sdk.getComponent("login.LoginHeader");
const LoginFooter = sdk.getComponent("login.LoginFooter"); const LoginFooter = sdk.getComponent("login.LoginFooter");
const ServerConfig = sdk.getComponent("login.ServerConfig"); const ServerConfig = sdk.getComponent("login.ServerConfig");

View file

@ -18,7 +18,7 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
@ -96,7 +96,7 @@ module.exports = React.createClass({
).then((data) => { ).then((data) => {
this.props.onLoggedIn(data); this.props.onLoggedIn(data);
}, (error) => { }, (error) => {
if(this._unmounted) { if (this._unmounted) {
return; return;
} }
let errorText; let errorText;
@ -113,14 +113,14 @@ module.exports = React.createClass({
<div className="mx_Login_smallError"> <div className="mx_Login_smallError">
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.', { _t('Please note you are logging into the %(hs)s server, not matrix.org.',
{ {
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, '') hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
}) })
} }
</div> </div>
</div> </div>
); );
} else { } else {
errorText = _t('Incorrect username and/or password.'); errorText = _t('Incorrect username and/or password.');
} }
} else { } else {
// other errors, not specific to doing a password login // other errors, not specific to doing a password login
@ -136,7 +136,7 @@ module.exports = React.createClass({
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403, loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
}); });
}).finally(() => { }).finally(() => {
if(this._unmounted) { if (this._unmounted) {
return; return;
} }
this.setState({ this.setState({
@ -272,17 +272,19 @@ module.exports = React.createClass({
!this.state.enteredHomeserverUrl.startsWith("http")) !this.state.enteredHomeserverUrl.startsWith("http"))
) { ) {
errorText = <span> errorText = <span>
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + {
_t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
"Either use HTTPS or <a>enable unsafe scripts</a>.", "Either use HTTPS or <a>enable unsafe scripts</a>.",
/<a>(.*?)<\/a>/, {},
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }, { 'a': (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } },
) } ) }
</span>; </span>;
} else { } else {
errorText = <span> errorText = <span>
{ _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.", {
/<a>(.*?)<\/a>/, _t("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }, {},
{ 'a': (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } },
) } ) }
</span>; </span>;
} }
@ -293,7 +295,7 @@ module.exports = React.createClass({
componentForStep: function(step) { componentForStep: function(step) {
switch (step) { switch (step) {
case 'm.login.password': case 'm.login.password': {
const PasswordLogin = sdk.getComponent('login.PasswordLogin'); const PasswordLogin = sdk.getComponent('login.PasswordLogin');
return ( return (
<PasswordLogin <PasswordLogin
@ -309,12 +311,14 @@ module.exports = React.createClass({
hsUrl={this.state.enteredHomeserverUrl} hsUrl={this.state.enteredHomeserverUrl}
/> />
); );
case 'm.login.cas': }
case 'm.login.cas': {
const CasLogin = sdk.getComponent('login.CasLogin'); const CasLogin = sdk.getComponent('login.CasLogin');
return ( return (
<CasLogin onSubmit={this.onCasLogin} /> <CasLogin onSubmit={this.onCasLogin} />
); );
default: }
default: {
if (!step) { if (!step) {
return; return;
} }
@ -323,11 +327,12 @@ module.exports = React.createClass({
{ _t('Sorry, this homeserver is using a login which is not recognised ') }({ step }) { _t('Sorry, this homeserver is using a login which is not recognised ') }({ step })
</div> </div>
); );
}
} }
}, },
_onLanguageChange: function(newLang) { _onLanguageChange: function(newLang) {
if(languageHandler.getCurrentLanguage() !== newLang) { if (languageHandler.getCurrentLanguage() !== newLang) {
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
PlatformPeg.get().reload(); PlatformPeg.get().reload();
} }
@ -388,8 +393,7 @@ module.exports = React.createClass({
const theme = SettingsStore.getValue("theme"); const theme = SettingsStore.getValue("theme");
if (theme !== "status") { if (theme !== "status") {
header = <h2>{ _t('Sign in') }</h2>; header = <h2>{ _t('Sign in') }</h2>;
} } else {
else {
if (!this.state.errorText) { if (!this.state.errorText) {
header = <h2>{ _t('Sign in to get started') }</h2>; header = <h2>{ _t('Sign in to get started') }</h2>;
} }

View file

@ -399,8 +399,7 @@ module.exports = React.createClass({
// FIXME: remove hardcoded Status team tweaks at some point // FIXME: remove hardcoded Status team tweaks at some point
if (theme === 'status' && this.state.errorText) { if (theme === 'status' && this.state.errorText) {
header = <div className="mx_Login_error">{ this.state.errorText }</div>; header = <div className="mx_Login_error">{ this.state.errorText }</div>;
} } else {
else {
header = <h2>{ _t('Create an account') }</h2>; header = <h2>{ _t('Create an account') }</h2>;
if (this.state.errorText) { if (this.state.errorText) {
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>; errorText = <div className="mx_Login_error">{ this.state.errorText }</div>;

View file

@ -54,7 +54,7 @@ export default React.createClass({
const deviceInfo = r[userId][deviceId]; const deviceInfo = r[userId][deviceId];
if(!deviceInfo) { if (!deviceInfo) {
console.warn(`No details found for device ${userId}:${deviceId}`); console.warn(`No details found for device ${userId}:${deviceId}`);
this.props.onFinished(false); this.props.onFinished(false);

View file

@ -18,7 +18,7 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
@ -45,9 +45,10 @@ export default React.createClass({
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = ( bugreport = (
<p> <p>
{ _tJsx( { _t(
"Otherwise, <a>click here</a> to send a bug report.", "Otherwise, <a>click here</a> to send a bug report.",
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a>, {},
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
) } ) }
</p> </p>
); );

View file

@ -21,7 +21,7 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames'; import classnames from 'classnames';
import KeyCode from '../../../KeyCode'; import KeyCode from '../../../KeyCode';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
// The amount of time to wait for further changes to the input username before // The amount of time to wait for further changes to the input username before
// sending a request to the server // sending a request to the server
@ -267,24 +267,21 @@ export default React.createClass({
</div> </div>
{ usernameIndicator } { usernameIndicator }
<p> <p>
{ _tJsx( { _t(
'This will be your account name on the <span></span> ' + 'This will be your account name on the <span></span> ' +
'homeserver, or you can pick a <a>different server</a>.', 'homeserver, or you can pick a <a>different server</a>.',
[ {},
/<span><\/span>/, {
/<a>(.*?)<\/a>/, 'span': <span>{ this.props.homeserverUrl }</span>,
], 'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
[ },
(sub) => <span>{ this.props.homeserverUrl }</span>,
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
],
) } ) }
</p> </p>
<p> <p>
{ _tJsx( { _t(
'If you already have a Matrix account you can <a>log in</a> instead.', 'If you already have a Matrix account you can <a>log in</a> instead.',
/<a>(.*?)<\/a>/, {},
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a>], { 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
) } ) }
</p> </p>
{ auth } { auth }

View file

@ -19,9 +19,9 @@ export default class AppPermission extends React.Component {
const searchParams = new URLSearchParams(wurl.search); const searchParams = new URLSearchParams(wurl.search);
if(this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) { if (this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
curl = url.parse(searchParams.get('url')); curl = url.parse(searchParams.get('url'));
if(curl) { if (curl) {
curl.search = curl.query = ""; curl.search = curl.query = "";
curlString = curl.format(); curlString = curl.format();
} }
@ -34,7 +34,7 @@ export default class AppPermission extends React.Component {
} }
isScalarWurl(wurl) { isScalarWurl(wurl) {
if(wurl && wurl.hostname && ( if (wurl && wurl.hostname && (
wurl.hostname === 'scalar.vector.im' || wurl.hostname === 'scalar.vector.im' ||
wurl.hostname === 'scalar-staging.riot.im' || wurl.hostname === 'scalar-staging.riot.im' ||
wurl.hostname === 'scalar-develop.riot.im' || wurl.hostname === 'scalar-develop.riot.im' ||

View file

@ -22,6 +22,7 @@ import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarAuthClient from '../../../ScalarAuthClient';
import TintableSvgButton from './TintableSvgButton';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
@ -283,7 +284,7 @@ export default React.createClass({
formatAppTileName() { formatAppTileName() {
let appTileName = "No name"; let appTileName = "No name";
if(this.props.name && this.props.name.trim()) { if (this.props.name && this.props.name.trim()) {
appTileName = this.props.name.trim(); appTileName = this.props.name.trim();
} }
return appTileName; return appTileName;
@ -371,9 +372,9 @@ export default React.createClass({
// editing is done in scalar // editing is done in scalar
const showEditButton = Boolean(this._scalarClient && this._canUserModify()); const showEditButton = Boolean(this._scalarClient && this._canUserModify());
const deleteWidgetLabel = this._deleteWidgetLabel(); const deleteWidgetLabel = this._deleteWidgetLabel();
let deleteIcon = 'img/cancel.svg'; let deleteIcon = 'img/cancel_green.svg';
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget'; let deleteClasses = 'mx_AppTileMenuBarWidget';
if(this._canUserModify()) { if (this._canUserModify()) {
deleteIcon = 'img/icon-delete-pink.svg'; deleteIcon = 'img/icon-delete-pink.svg';
deleteClasses += ' mx_AppTileMenuBarWidgetDelete'; deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
} }
@ -384,22 +385,23 @@ export default React.createClass({
<b>{ this.formatAppTileName() }</b> <b>{ this.formatAppTileName() }</b>
<span className="mx_AppTileMenuBarWidgets"> <span className="mx_AppTileMenuBarWidgets">
{ /* Edit widget */ } { /* Edit widget */ }
{ showEditButton && <img { showEditButton && <TintableSvgButton
src="img/edit_green.svg" src="img/edit_green.svg"
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding" className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
width="8" height="8"
alt={_t('Edit')}
title={_t('Edit')} title={_t('Edit')}
onClick={this._onEditClick} onClick={this._onEditClick}
width="10"
height="10"
/> } /> }
{ /* Delete widget */ } { /* Delete widget */ }
<img src={deleteIcon} <TintableSvgButton
className={deleteClasses} src={deleteIcon}
width="8" height="8" className={deleteClasses}
alt={_t(deleteWidgetLabel)} title={_t(deleteWidgetLabel)}
title={_t(deleteWidgetLabel)} onClick={this._onDeleteClick}
onClick={this._onDeleteClick} width="10"
height="10"
/> />
</span> </span>
</div> </div>

View file

@ -41,8 +41,8 @@ export default class LanguageDropdown extends React.Component {
componentWillMount() { componentWillMount() {
languageHandler.getAllLanguagesFromJson().then((langs) => { languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b) { langs.sort(function(a, b) {
if(a.label < b.label) return -1; if (a.label < b.label) return -1;
if(a.label > b.label) return 1; if (a.label > b.label) return 1;
return 0; return 0;
}); });
this.setState({langs}); this.setState({langs});
@ -57,7 +57,7 @@ export default class LanguageDropdown extends React.Component {
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
if (language) { if (language) {
this.props.onOptionChange(language); this.props.onOptionChange(language);
}else { } else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language); this.props.onOptionChange(language);
} }

View file

@ -216,7 +216,7 @@ module.exports = React.createClass({
// are there only to show translators to non-English languages // are there only to show translators to non-English languages
// that the verb is conjugated to plural or singular Subject. // that the verb is conjugated to plural or singular Subject.
let res = null; let res = null;
switch(t) { switch (t) {
case "joined": case "joined":
res = (userCount > 1) res = (userCount > 1)
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats }) ? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats })
@ -304,7 +304,7 @@ module.exports = React.createClass({
return items[0]; return items[0];
} else if (remaining > 0) { } else if (remaining > 0) {
items = items.slice(0, itemLimit); items = items.slice(0, itemLimit);
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ) return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
} else { } else {
const lastItem = items.pop(); const lastItem = items.pop();
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });

View file

@ -0,0 +1,61 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import TintableSvg from './TintableSvg';
export default class TintableSvgButton extends React.Component {
constructor(props) {
super(props);
}
render() {
let classes = "mx_TintableSvgButton";
if (this.props.className) {
classes += " " + this.props.className;
}
return (
<span
width={this.props.width}
height={this.props.height}
className={classes}>
<TintableSvg
src={this.props.src}
width={this.props.width}
height={this.props.height}
></TintableSvg>
<span
title={this.props.title}
onClick={this.props.onClick} />
</span>
);
}
}
TintableSvgButton.propTypes = {
src: PropTypes.string,
title: PropTypes.string,
className: PropTypes.string,
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
TintableSvgButton.defaultProps = {
onClick: function() {},
};

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
const DIV_ID = 'mx_recaptcha'; const DIV_ID = 'mx_recaptcha';
@ -67,10 +67,10 @@ module.exports = React.createClass({
// * jumping straight to a hosted captcha page (but we don't support that yet) // * jumping straight to a hosted captcha page (but we don't support that yet)
// * embedding the captcha in an iframe (if that works) // * embedding the captcha in an iframe (if that works)
// * using a better captcha lib // * using a better captcha lib
ReactDOM.render(_tJsx( ReactDOM.render(_t(
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>", "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
/<a>(.*?)<\/a>/, {},
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning); { 'a': (sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }}), warning);
this.refs.recaptchaContainer.appendChild(warning); this.refs.recaptchaContainer.appendChild(warning);
} else { } else {
const scriptTag = document.createElement('script'); const scriptTag = document.createElement('script');

View file

@ -20,7 +20,7 @@ import url from 'url';
import classnames from 'classnames'; import classnames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
@ -256,7 +256,10 @@ export const EmailIdentityAuthEntry = React.createClass({
} else { } else {
return ( return (
<div> <div>
<p>{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => <i>{this.props.inputs.emailAddress}</i>) }</p> <p>{ _t("An email has been sent to %(emailAddress)s",
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
) }
</p>
<p>{ _t("Please check your email to continue registration.") }</p> <p>{ _t("Please check your email to continue registration.") }</p>
</div> </div>
); );
@ -370,7 +373,10 @@ export const MsisdnAuthEntry = React.createClass({
}); });
return ( return (
<div> <div>
<p>{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => <i>{this._msisdn}</i>) }</p> <p>{ _t("A text message has been sent to %(msisdn)s",
{ msisdn: <i>this._msisdn</i> },
) }
</p>
<p>{ _t("Please enter the code it contains:") }</p> <p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper"> <div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}> <form onSubmit={this._onFormSubmit}>

View file

@ -122,7 +122,7 @@ class PasswordLogin extends React.Component {
mx_Login_field_disabled: disabled, mx_Login_field_disabled: disabled,
}; };
switch(loginType) { switch (loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL: case PasswordLogin.LOGIN_FIELD_EMAIL:
classes.mx_Login_email = true; classes.mx_Login_email = true;
return <input return <input
@ -144,9 +144,9 @@ class PasswordLogin extends React.Component {
type="text" type="text"
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
placeholder={ SdkConfig.get().disable_custom_urls ? placeholder={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", { _t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, '') hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("User name")} }) : _t("User name")}
value={this.state.username} value={this.state.username}
autoFocus autoFocus

View file

@ -282,7 +282,7 @@ module.exports = React.createClass({
const emailSection = ( const emailSection = (
<div> <div>
<input type="text" ref="email" <input type="text" ref="email"
autoFocus={true} placeholder={ emailPlaceholder } autoFocus={true} placeholder={emailPlaceholder}
defaultValue={this.props.defaultEmail} defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}} onBlur={function() {self.validateField(FIELD_EMAIL);}}

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk'; import { ContentRepo } from 'matrix-js-sdk';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
@ -67,24 +67,17 @@ module.exports = React.createClass({
'crop', 'crop',
); );
// it sucks that _tJsx doesn't support normal _t substitutions :((
return ( return (
<div className="mx_RoomAvatarEvent"> <div className="mx_RoomAvatarEvent">
{ _tJsx('%(senderDisplayName)s changed the room avatar to <img/>', { _t('%(senderDisplayName)s changed the room avatar to <img/>',
[ { senderDisplayName: senderDisplayName },
/%\(senderDisplayName\)s/, {
/<img\/>/, 'img': () =>
], <AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
[ onClick={this.onAvatarClick.bind(this, name)}>
(sub) => senderDisplayName, <BaseAvatar width={14} height={14} url={url} name={name} />
(sub) => </AccessibleButton>,
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar" })
onClick={this.onAvatarClick.bind(this, name)}>
<BaseAvatar width={14} height={14} url={url}
name={name} />
</AccessibleButton>,
],
)
} }
</div> </div>
); );

View file

@ -19,7 +19,7 @@
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import Flair from '../elements/Flair.js'; import Flair from '../elements/Flair.js';
import { _tJsx } from '../../../languageHandler'; import { _t, substitute } from '../../../languageHandler';
export default function SenderProfile(props) { export default function SenderProfile(props) {
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
@ -42,22 +42,28 @@ export default function SenderProfile(props) {
: null, : null,
]; ];
let content = ''; let content;
if (props.text) {
if(props.text) { content = _t(props.text, { senderName: () => nameElem });
// Replace senderName, and wrap surrounding text in spans with the right class
content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [
p1 ? <span className='mx_SenderProfile_aux'>{ p1 }</span> : null,
nameElem,
p2 ? <span className='mx_SenderProfile_aux'>{ p2 }</span> : null,
]);
} else { } else {
content = nameElem; // There is nothing to translate here, so call substitute() instead
content = substitute('%(senderName)s', { senderName: () => nameElem });
} }
// The text surrounding the user name must be wrapped in order for it to have the correct opacity.
// It is not possible to wrap the whole thing, because the user name might contain flair which should
// be shown at full opacity. Sadly CSS does not make it possible to "reset" opacity so we have to do it
// in parts like this. Sometimes CSS makes me a sad panda :-(
// XXX: This could be avoided if the actual colour is set, rather than faking it with opacity
return ( return (
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}> <div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
{ content } { content.props.children[0] ?
<span className='mx_SenderProfile_aux'>{ content.props.children[0] }</span> : ''
}
{ content.props.children[1] }
{ content.props.children[2] ?
<span className='mx_SenderProfile_aux'>{ content.props.children[2] }</span> : ''
}
</div> </div>
); );
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
const React = require('react'); const React = require('react');
const sdk = require("../../../index"); const sdk = require("../../../index");
import { _t, _tJsx } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
@ -42,11 +42,11 @@ module.exports = React.createClass({
let previewsForAccount = null; let previewsForAccount = null;
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) { if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
previewsForAccount = ( previewsForAccount = (
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>) _t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
); );
} else { } else {
previewsForAccount = ( previewsForAccount = (
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>) _t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
); );
} }
@ -63,14 +63,14 @@ module.exports = React.createClass({
</label> </label>
); );
} else { } else {
let str = "URL previews are enabled by default for participants in this room."; let str = _td("URL previews are enabled by default for participants in this room.");
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled")) { if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled")) {
str = "URL previews are disabled by default for participants in this room."; str = _td("URL previews are disabled by default for participants in this room.");
} }
previewsForRoom = (<label>{ _t(str) }</label>); previewsForRoom = (<label>{ _t(str) }</label>);
} }
let previewsForRoomAccount = ( const previewsForRoomAccount = (
<SettingsFlag name="urlPreviewsEnabled" <SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM_ACCOUNT} level={SettingLevel.ROOM_ACCOUNT}
roomId={this.props.room.roomId} roomId={this.props.room.roomId}

View file

@ -133,7 +133,7 @@ module.exports = React.createClass({
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '', '$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
}; };
if(app.data) { if (app.data) {
Object.keys(app.data).forEach((key) => { Object.keys(app.data).forEach((key) => {
params['$' + key] = app.data[key]; params['$' + key] = app.data[key];
}); });
@ -177,7 +177,7 @@ module.exports = React.createClass({
_canUserModify: function() { _canUserModify: function() {
try { try {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId); return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
} catch(err) { } catch (err) {
console.error(err); console.error(err);
return false; return false;
} }

View file

@ -21,7 +21,7 @@ import sdk from '../../../index';
import dis from "../../../dispatcher"; import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils'; import ObjectUtils from '../../../ObjectUtils';
import AppsDrawer from './AppsDrawer'; import AppsDrawer from './AppsDrawer';
import { _t, _tJsx} from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
@ -99,13 +99,13 @@ module.exports = React.createClass({
supportedText = _t(" (unsupported)"); supportedText = _t(" (unsupported)");
} else { } else {
joinNode = (<span> joinNode = (<span>
{ _tJsx( { _t(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.", "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/], {},
[ {
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>, 'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>, 'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
], },
) } ) }
</span>); </span>);
} }

View file

@ -33,22 +33,30 @@ const ObjectUtils = require('../../../ObjectUtils');
const eventTileTypes = { const eventTileTypes = {
'm.room.message': 'messages.MessageEvent', 'm.room.message': 'messages.MessageEvent',
'm.room.member': 'messages.TextualEvent',
'm.call.invite': 'messages.TextualEvent', 'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent', 'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent', 'm.call.hangup': 'messages.TextualEvent',
};
const stateEventTileTypes = {
'm.room.member': 'messages.TextualEvent',
'm.room.name': 'messages.TextualEvent', 'm.room.name': 'messages.TextualEvent',
'm.room.avatar': 'messages.RoomAvatarEvent', 'm.room.avatar': 'messages.RoomAvatarEvent',
'm.room.topic': 'messages.TextualEvent',
'm.room.third_party_invite': 'messages.TextualEvent', 'm.room.third_party_invite': 'messages.TextualEvent',
'm.room.history_visibility': 'messages.TextualEvent', 'm.room.history_visibility': 'messages.TextualEvent',
'm.room.encryption': 'messages.TextualEvent', 'm.room.encryption': 'messages.TextualEvent',
'm.room.topic': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent', 'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events' : 'messages.TextualEvent', 'm.room.pinned_events': 'messages.TextualEvent',
'im.vector.modular.widgets': 'messages.TextualEvent', 'im.vector.modular.widgets': 'messages.TextualEvent',
}; };
function getHandlerTile(ev) {
const type = ev.getType();
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
}
const MAX_READ_AVATARS = 5; const MAX_READ_AVATARS = 5;
// Our component structure for EventTiles on the timeline is: // Our component structure for EventTiles on the timeline is:
@ -433,7 +441,7 @@ module.exports = withMatrixClient(React.createClass({
// Info messages are basically information about commands processed on a room // Info messages are basically information about commands processed on a room
const isInfoMessage = (eventType !== 'm.room.message'); const isInfoMessage = (eventType !== 'm.room.message');
const EventTileType = sdk.getComponent(eventTileTypes[eventType]); const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
// This shouldn't happen: the caller should check we support this type // This shouldn't happen: the caller should check we support this type
// before trying to instantiate us // before trying to instantiate us
if (!EventTileType) { if (!EventTileType) {
@ -600,8 +608,10 @@ module.exports = withMatrixClient(React.createClass({
module.exports.haveTileForEvent = function(e) { module.exports.haveTileForEvent = function(e) {
// Only messages have a tile (black-rectangle) if redacted // Only messages have a tile (black-rectangle) if redacted
if (e.isRedacted() && e.getType() !== 'm.room.message') return false; if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
if (eventTileTypes[e.getType()] == undefined) return false;
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') { const handler = getHandlerTile(e);
if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== ''; return TextForEvent.textForEvent(e) !== '';
} else { } else {
return true; return true;

View file

@ -562,7 +562,7 @@ module.exports = withMatrixClient(React.createClass({
onMemberAvatarClick: function() { onMemberAvatarClick: function() {
const member = this.props.member; const member = this.props.member;
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url; const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
if(!avatarUrl) return; if (!avatarUrl) return;
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl); const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
const ImageView = sdk.getComponent("elements.ImageView"); const ImageView = sdk.getComponent("elements.ImageView");

View file

@ -111,10 +111,10 @@ export default class MessageComposer extends React.Component {
</div> </div>
), ),
onFinished: (shouldUpload) => { onFinished: (shouldUpload) => {
if(shouldUpload) { if (shouldUpload) {
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
if (files) { if (files) {
for(let i=0; i<files.length; i++) { for (let i=0; i<files.length; i++) {
this.props.uploadFile(files[i]); this.props.uploadFile(files[i]);
} }
} }

View file

@ -427,7 +427,7 @@ export default class MessageComposerInput extends React.Component {
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
// The first matched group includes just the matched plaintext emoji // The first matched group includes just the matched plaintext emoji
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset)); const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
if(emojiMatch) { if (emojiMatch) {
// plaintext -> hex unicode // plaintext -> hex unicode
const emojiUc = asciiList[emojiMatch[1]]; const emojiUc = asciiList[emojiMatch[1]];
// hex unicode -> shortname -> actual unicode // hex unicode -> shortname -> actual unicode
@ -689,7 +689,7 @@ export default class MessageComposerInput extends React.Component {
} }
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState); const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
if( if (
['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'] ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item']
.includes(currentBlockType) .includes(currentBlockType)
) { ) {

View file

@ -389,7 +389,7 @@ module.exports = React.createClass({
let rightRow; let rightRow;
let manageIntegsButton; let manageIntegsButton;
if(this.props.room && this.props.room.roomId && this.props.inRoom) { if (this.props.room && this.props.room.roomId && this.props.inRoom) {
manageIntegsButton = <ManageIntegsButton manageIntegsButton = <ManageIntegsButton
roomId={this.props.room.roomId} roomId={this.props.room.roomId}
/>; />;

View file

@ -18,12 +18,10 @@ limitations under the License.
'use strict'; 'use strict';
const React = require("react"); const React = require("react");
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
const GeminiScrollbar = require('react-gemini-scrollbar'); const GeminiScrollbar = require('react-gemini-scrollbar');
const MatrixClientPeg = require("../../../MatrixClientPeg"); const MatrixClientPeg = require("../../../MatrixClientPeg");
const CallHandler = require('../../../CallHandler'); const CallHandler = require('../../../CallHandler');
const RoomListSorter = require("../../../RoomListSorter");
const Unread = require('../../../Unread');
const dis = require("../../../dispatcher"); const dis = require("../../../dispatcher");
const sdk = require('../../../index'); const sdk = require('../../../index');
const rate_limited_func = require('../../../ratelimitedfunc'); const rate_limited_func = require('../../../ratelimitedfunc');
@ -486,28 +484,25 @@ module.exports = React.createClass({
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
switch (section) { switch (section) {
case 'im.vector.fake.direct': case 'im.vector.fake.direct':
return <div className="mx_RoomList_emptySubListTip"> return <div className="mx_RoomList_emptySubListTip">
{ _tJsx( { _t(
"Press <StartChatButton> to start a chat with someone", "Press <StartChatButton> to start a chat with someone",
[/<StartChatButton>/], {},
[ { 'StartChatButton': <StartChatButton size="16" callout={true} /> },
(sub) => <StartChatButton size="16" callout={true} />,
],
) } ) }
</div>; </div>;
case 'im.vector.fake.recent': case 'im.vector.fake.recent':
return <div className="mx_RoomList_emptySubListTip"> return <div className="mx_RoomList_emptySubListTip">
{ _tJsx( { _t(
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+ "You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
" <RoomDirectoryButton> to browse the directory", " <RoomDirectoryButton> to browse the directory",
[/<CreateRoomButton>/, /<RoomDirectoryButton>/], {},
[ {
(sub) => <CreateRoomButton size="16" callout={true} />, 'CreateRoomButton': <CreateRoomButton size="16" callout={true} />,
(sub) => <RoomDirectoryButton size="16" callout={true} />, 'RoomDirectoryButton': <RoomDirectoryButton size="16" callout={true} />,
], },
) } ) }
</div>; </div>;
} }

View file

@ -21,7 +21,7 @@ const React = require('react');
const sdk = require('../../../index'); const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg'); const MatrixClientPeg = require('../../../MatrixClientPeg');
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomPreviewBar', displayName: 'RoomPreviewBar',
@ -135,13 +135,13 @@ module.exports = React.createClass({
{ _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) } { _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
</div> </div>
<div className="mx_RoomPreviewBar_join_text"> <div className="mx_RoomPreviewBar_join_text">
{ _tJsx( { _t(
'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?', 'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?',
[/<acceptText>(.*?)<\/acceptText>/, /<declineText>(.*?)<\/declineText>/], {},
[ {
(sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>, 'acceptText': (sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>,
(sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>, 'declineText': (sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
], },
) } ) }
</div> </div>
{ emailMatchBlock } { emailMatchBlock }
@ -165,13 +165,13 @@ module.exports = React.createClass({
let actionText; let actionText;
if (kicked) { if (kicked) {
if(roomName) { if (roomName) {
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
} else { } else {
actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName}); actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName});
} }
} else if (banned) { } else if (banned) {
if(roomName) { if (roomName) {
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
} else { } else {
actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName}); actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName});
@ -211,9 +211,9 @@ module.exports = React.createClass({
<div className="mx_RoomPreviewBar_join_text"> <div className="mx_RoomPreviewBar_join_text">
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') } { name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
<br /> <br />
{ _tJsx("<a>Click here</a> to join the discussion!", { _t("<a>Click here</a> to join the discussion!",
/<a>(.*?)<\/a>/, {},
(sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a>, { 'a': (sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a> },
) } ) }
</div> </div>
</div> </div>

View file

@ -17,7 +17,7 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import React from 'react'; import React from 'react';
import { _t, _tJsx, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -309,9 +309,9 @@ module.exports = React.createClass({
} }
// url preview settings // url preview settings
let ps = this.saveUrlPreviewSettings(); const ps = this.saveUrlPreviewSettings();
if (ps.length > 0) { if (ps.length > 0) {
ps.map(p => promises.push(p)); ps.map((p) => promises.push(p));
} }
// related groups // related groups
@ -584,7 +584,7 @@ module.exports = React.createClass({
const roomState = this.props.room.currentState; const roomState = this.props.room.currentState;
const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId); const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
let settings = ( const settings = (
<SettingsFlag name="blacklistUnverifiedDevices" <SettingsFlag name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE} level={SettingLevel.ROOM_DEVICE}
roomId={this.props.room.roomId} roomId={this.props.room.roomId}
@ -628,9 +628,7 @@ module.exports = React.createClass({
const ColorSettings = sdk.getComponent("room_settings.ColorSettings"); const ColorSettings = sdk.getComponent("room_settings.ColorSettings");
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings"); const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
const EditableText = sdk.getComponent('elements.EditableText');
const PowerSelector = sdk.getComponent('elements.PowerSelector'); const PowerSelector = sdk.getComponent('elements.PowerSelector');
const Loader = sdk.getComponent("elements.Spinner");
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState; const roomState = this.props.room.currentState;
@ -749,9 +747,9 @@ module.exports = React.createClass({
} }
}); });
var tagsSection = null; let tagsSection = null;
if (canSetTag || self.state.tags) { if (canSetTag || self.state.tags) {
var tagsSection = tagsSection =
<div className="mx_RoomSettings_tags"> <div className="mx_RoomSettings_tags">
{ _t("Tagged as: ") }{ canSetTag ? { _t("Tagged as: ") }{ canSetTag ?
(tags.map(function(tag, i) { (tags.map(function(tag, i) {
@ -781,10 +779,10 @@ module.exports = React.createClass({
if (this.state.join_rule === "public" && aliasCount == 0) { if (this.state.join_rule === "public" && aliasCount == 0) {
addressWarning = addressWarning =
<div className="mx_RoomSettings_warning"> <div className="mx_RoomSettings_warning">
{ _tJsx( { _t(
'To link to a room it must have <a>an address</a>.', 'To link to a room it must have <a>an address</a>.',
/<a>(.*?)<\/a>/, {},
(sub) => <a href="#addresses">{ sub }</a>, { 'a': (sub) => <a href="#addresses">{ sub }</a> },
) } ) }
</div>; </div>;
} }
@ -931,7 +929,7 @@ module.exports = React.createClass({
{ Object.keys(events_levels).map(function(event_type, i) { { Object.keys(events_levels).map(function(event_type, i) {
let label = plEventsToLabels[event_type]; let label = plEventsToLabels[event_type];
if (label) label = _t(label); if (label) label = _t(label);
else label = _tJsx("To send events of type <eventType/>, you must be a", /<eventType\/>/, () => <code>{ event_type }</code>); else label = _t("To send events of type <eventType/>, you must be a", {}, { 'eventType': <code>{ event_type }</code> });
return ( return (
<div className="mx_RoomSettings_powerLevel" key={event_type}> <div className="mx_RoomSettings_powerLevel" key={event_type}>
<span className="mx_RoomSettings_powerLevelKey">{ label } </span> <span className="mx_RoomSettings_powerLevelKey">{ label } </span>

View file

@ -54,7 +54,7 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
return({ return ({
hover: false, hover: false,
badgeHover: false, badgeHover: false,
menuDisplayed: false, menuDisplayed: false,

View file

@ -39,7 +39,7 @@ module.exports = React.createClass({
}, },
onResize: function(e) { onResize: function(e) {
if(this.props.onResize) { if (this.props.onResize) {
this.props.onResize(e); this.props.onResize(e);
} }
}, },

View file

@ -460,6 +460,8 @@
"New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)", "New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)",
"You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.", "You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.",
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.", "You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
"URL previews are enabled by default for participants in this room.": "URL previews are enabled by default for participants in this room.",
"URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.",
"URL Previews": "URL Previews", "URL Previews": "URL Previews",
"Error decrypting audio": "Error decrypting audio", "Error decrypting audio": "Error decrypting audio",
"Error decrypting attachment": "Error decrypting attachment", "Error decrypting attachment": "Error decrypting attachment",
@ -752,16 +754,16 @@
"You have no visible notifications": "You have no visible notifications", "You have no visible notifications": "You have no visible notifications",
"Scroll to bottom of page": "Scroll to bottom of page", "Scroll to bottom of page": "Scroll to bottom of page",
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
"<a>Show devices</a> or <a>cancel all</a>.": "<a>Show devices</a> or <a>cancel all</a>.", "<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.": "<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
"Some of your messages have not been sent.": "Some of your messages have not been sent.", "Some of your messages have not been sent.": "Some of your messages have not been sent.",
"<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.": "<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.", "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
"Warning": "Warning", "Warning": "Warning",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"%(count)s new messages|other": "%(count)s new messages", "%(count)s new messages|other": "%(count)s new messages",
"%(count)s new messages|one": "%(count)s new message", "%(count)s new messages|one": "%(count)s new message",
"Active call": "Active call", "Active call": "Active call",
"There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?": "There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?", "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
"Failed to upload file": "Failed to upload file", "Failed to upload file": "Failed to upload file",

View file

@ -34,12 +34,9 @@ export function _td(s) {
return s; return s;
} }
// The translation function. This is just a simple wrapper to counterpart, // Wrapper for counterpart's translation function so that it handles nulls and undefineds properly
// but exists mostly because we must use the same counterpart instance // Takes the same arguments as counterpart.translate()
// between modules (ie. here (react-sdk) and the app (riot-web), and if we function safeCounterpartTranslate(...args) {
// just import counterpart and use it directly, we end up using a different
// instance.
export function _t(...args) {
// Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191 // Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191
// The interpolation library that counterpart uses does not support undefined/null // The interpolation library that counterpart uses does not support undefined/null
// values and instead will throw an error. This is a problem since everywhere else // values and instead will throw an error. This is a problem since everywhere else
@ -50,11 +47,11 @@ export function _t(...args) {
if (args[1] && typeof args[1] === 'object') { if (args[1] && typeof args[1] === 'object') {
Object.keys(args[1]).forEach((k) => { Object.keys(args[1]).forEach((k) => {
if (args[1][k] === undefined) { if (args[1][k] === undefined) {
console.warn("_t called with undefined interpolation name: " + k); console.warn("safeCounterpartTranslate called with undefined interpolation name: " + k);
args[1][k] = 'undefined'; args[1][k] = 'undefined';
} }
if (args[1][k] === null) { if (args[1][k] === null) {
console.warn("_t called with null interpolation name: " + k); console.warn("safeCounterpartTranslate called with null interpolation name: " + k);
args[1][k] = 'null'; args[1][k] = 'null';
} }
}); });
@ -63,75 +60,136 @@ export function _t(...args) {
} }
/* /*
* Translates stringified JSX into translated JSX. E.g * Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
* _tJsx( * @param {string} text The untranslated text, e.g "click <a>here</a> now to %(foo)s".
* "click <a href=''>here</a> now", * @param {object} variables Variable substitutions, e.g { foo: 'bar' }
* /<a href=''>(.*?)<\/a>/, * @param {object} tags Tag substitutions e.g. { 'a': (sub) => <a>{sub}</a> }
* (sub) => { return <a href=''>{ sub }</a>; }
* );
* *
* @param {string} jsxText The untranslated stringified JSX e.g "click <a href=''>here</a> now". * In both variables and tags, the values to substitute with can be either simple strings, React components,
* This will be translated by passing the string through to _t(...) * or functions that return the value to use in the substitution (e.g. return a React component). In case of
* a tag replacement, the function receives as the argument the text inside the element corresponding to the tag.
* *
* @param {RegExp|RegExp[]} patterns A regexp to match against the translated text. * Use tag substitutions if you need to translate text between tags (e.g. "<a>Click here!</a>"), otherwise
* The captured groups from the regexp will be fed to 'sub'. * you will end up with literal "<a>" in your output, rather than HTML. Note that you can also use variable
* Only the captured groups will be included in the output, the match itself is discarded. * substitution to insert React components, but you can't use it to translate text between tags.
* If multiple RegExps are provided, the function at the same position will be called. The
* match will always be done from left to right, so the 2nd RegExp will be matched against the
* remaining text from the first RegExp.
* *
* @param {Function|Function[]} subs A function which will be called * @return a React <span> component if any non-strings were used in substitutions, otherwise a string
* with multiple args, each arg representing a captured group of the matching regexp.
* This function must return a JSX node.
*
* @return a React <span> component containing the generated text
*/ */
export function _tJsx(jsxText, patterns, subs) { export function _t(text, variables, tags) {
// convert everything to arrays // Don't do subsitutions in counterpart. We handle it ourselves so we can replace with React components
if (patterns instanceof RegExp) { // However, still pass the variables to counterpart so that it can choose the correct plural if count is given
patterns = [patterns]; // It is enough to pass the count variable, but in the future counterpart might make use of other information too
} const args = Object.assign({ interpolate: false }, variables);
if (subs instanceof Function) {
subs = [subs];
}
// sanity checks
if (subs.length !== patterns.length || subs.length < 1) {
throw new Error(`_tJsx: programmer error. expected number of RegExps == number of Functions: ${subs.length} != ${patterns.length}`);
}
for (let i = 0; i < subs.length; i++) {
if (!(patterns[i] instanceof RegExp)) {
throw new Error(`_tJsx: programmer error. expected RegExp for text: ${jsxText}`);
}
if (!(subs[i] instanceof Function)) {
throw new Error(`_tJsx: programmer error. expected Function for text: ${jsxText}`);
}
}
// The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution) // The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution)
const tJsxText = _t(jsxText, {interpolate: false}); const translated = safeCounterpartTranslate(text, args);
const output = [tJsxText];
return substitute(translated, variables, tags);
}
/*
* Similar to _t(), except only does substitutions, and no translation
* @param {string} text The text, e.g "click <a>here</a> now to %(foo)s".
* @param {object} variables Variable substitutions, e.g { foo: 'bar' }
* @param {object} tags Tag substitutions e.g. { 'a': (sub) => <a>{sub}</a> }
*
* The values to substitute with can be either simple strings, or functions that return the value to use in
* the substitution (e.g. return a React component). In case of a tag replacement, the function receives as
* the argument the text inside the element corresponding to the tag.
*
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
export function substitute(text, variables, tags) {
const regexpMapping = {};
if (variables !== undefined) {
for (const variable in variables) {
regexpMapping[`%\\(${variable}\\)s`] = variables[variable];
}
}
if (tags !== undefined) {
for (const tag in tags) {
regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag];
}
}
return replaceByRegexes(text, regexpMapping);
}
/*
* Replace parts of a text using regular expressions
* @param {string} text The text on which to perform substitutions
* @param {object} mapping A mapping from regular expressions in string form to replacement string or a
* function which will receive as the argument the capture groups defined in the regexp. E.g.
* { 'Hello (.?) World': (sub) => sub.toUpperCase() }
*
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
export function replaceByRegexes(text, mapping) {
const output = [text];
// If we insert any components we need to wrap the output in a span. React doesn't like just an array of components.
let shouldWrapInSpan = false;
for (const regexpString in mapping) {
// TODO: Cache regexps
const regexp = new RegExp(regexpString);
for (let i = 0; i < patterns.length; i++) {
// convert the last element in 'output' into 3 elements (pre-text, sub function, post-text). // convert the last element in 'output' into 3 elements (pre-text, sub function, post-text).
// Rinse and repeat for other patterns (using post-text). // Rinse and repeat for other patterns (using post-text).
const inputText = output.pop(); const inputText = output.pop();
const match = inputText.match(patterns[i]); const match = inputText.match(regexp);
if (!match) { if (!match) {
throw new Error(`_tJsx: translator error. expected translation to match regexp: ${patterns[i]}`); output.push(inputText); // Push back input
// Missing matches is entirely possible because you might choose to show some variables only in the case
// of e.g. plurals. It's still a bit suspicious, and could be due to an error, so log it.
// However, not showing count is so common that it's not worth logging. And other commonly unused variables
// here, if there are any.
if (regexpString !== '%\\(count\\)s') {
console.log(`Could not find ${regexp} in ${inputText}`);
}
continue;
} }
const capturedGroups = match.slice(1); const capturedGroups = match.slice(2);
// Return the raw translation before the *match* followed by the return value of sub() followed // Return the raw translation before the *match* followed by the return value of sub() followed
// by the raw translation after the *match* (not captured group). // by the raw translation after the *match* (not captured group).
output.push(inputText.substr(0, match.index));
output.push(subs[i].apply(null, capturedGroups)); const head = inputText.substr(0, match.index);
output.push(inputText.substr(match.index + match[0].length)); if (head !== '') { // Don't push empty nodes, they are of no use
output.push(head);
}
let replaced;
// If substitution is a function, call it
if (mapping[regexpString] instanceof Function) {
replaced = mapping[regexpString].apply(null, capturedGroups);
} else {
replaced = mapping[regexpString];
}
// Here we also need to check that it actually is a string before comparing against one
// The head and tail are always strings
if (typeof replaced !== 'string' || replaced !== '') {
output.push(replaced);
}
if (typeof replaced === 'object') {
shouldWrapInSpan = true;
}
const tail = inputText.substr(match.index + match[0].length);
if (tail !== '') {
output.push(tail);
}
} }
// this is a bit of a fudge to avoid the 'Each child in an array or iterator if (shouldWrapInSpan) {
// should have a unique "key" prop' error: we explicitly pass the generated return React.createElement('span', null, ...output);
// nodes into React.createElement as children of a <span>. } else {
return React.createElement('span', null, ...output); return output.join('');
}
} }
// Allow overriding the text displayed when no translation exists // Allow overriding the text displayed when no translation exists

View file

@ -176,13 +176,21 @@ export default class SettingsStore {
* @return {*} The value, or null if not found * @return {*} The value, or null if not found
*/ */
static getValue(settingName, roomId = null, excludeDefault = false) { static getValue(settingName, roomId = null, excludeDefault = false) {
return SettingsStore.getValueAt(LEVEL_ORDER[0], settingName, roomId, false, excludeDefault); // Verify that the setting is actually a setting
if (!SETTINGS[settingName]) {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
}
const setting = SETTINGS[settingName];
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
return SettingsStore.getValueAt(levelOrder[0], settingName, roomId, false, excludeDefault);
} }
/** /**
* Gets a setting's value at a particular level, ignoring all levels that are more specific. * Gets a setting's value at a particular level, ignoring all levels that are more specific.
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to * @param {"device"|"room-device"|"room-account"|"account"|"room"|"config"|"default"} level The
* look at. * level to look at.
* @param {string} settingName The name of the setting to read. * @param {string} settingName The name of the setting to read.
* @param {String} roomId The room ID to read the setting value in, may be null. * @param {String} roomId The room ID to read the setting value in, may be null.
* @param {boolean} explicit If true, this method will not consider other levels, just the one * @param {boolean} explicit If true, this method will not consider other levels, just the one

View file

@ -15,12 +15,35 @@ limitations under the License.
*/ */
import SettingController from "./SettingController"; import SettingController from "./SettingController";
import MatrixClientPeg from '../../MatrixClientPeg';
// XXX: This feels wrong.
import PushProcessor from "matrix-js-sdk/lib/pushprocessor";
function isMasterRuleEnabled() {
// Return the value of the master push rule as a default
const processor = new PushProcessor(MatrixClientPeg.get());
const masterRule = processor.getPushRuleById(".m.rule.master");
if (!masterRule) {
console.warn("No master push rule! Notifications are disabled for this user.");
return false;
}
// Why enabled == false means "enabled" is beyond me.
return !masterRule.enabled;
}
export class NotificationsEnabledController extends SettingController { export class NotificationsEnabledController extends SettingController {
getValueOverride(level, roomId, calculatedValue) { getValueOverride(level, roomId, calculatedValue) {
const Notifier = require('../../Notifier'); // avoids cyclical references const Notifier = require('../../Notifier'); // avoids cyclical references
if (!Notifier.isPossible()) return false;
return calculatedValue && Notifier.isPossible(); if (calculatedValue === null) {
return isMasterRuleEnabled();
}
return calculatedValue;
} }
onChange(level, roomId, newValue) { onChange(level, roomId, newValue) {
@ -35,15 +58,22 @@ export class NotificationsEnabledController extends SettingController {
export class NotificationBodyEnabledController extends SettingController { export class NotificationBodyEnabledController extends SettingController {
getValueOverride(level, roomId, calculatedValue) { getValueOverride(level, roomId, calculatedValue) {
const Notifier = require('../../Notifier'); // avoids cyclical references const Notifier = require('../../Notifier'); // avoids cyclical references
if (!Notifier.isPossible()) return false;
return calculatedValue && Notifier.isEnabled(); if (calculatedValue === null) {
return isMasterRuleEnabled();
}
return calculatedValue;
} }
} }
export class AudioNotificationsEnabledController extends SettingController { export class AudioNotificationsEnabledController extends SettingController {
getValueOverride(level, roomId, calculatedValue) { getValueOverride(level, roomId, calculatedValue) {
const Notifier = require('../../Notifier'); // avoids cyclical references const Notifier = require('../../Notifier'); // avoids cyclical references
if (!Notifier.isPossible()) return false;
return calculatedValue && Notifier.isEnabled(); // Note: Audio notifications are *not* enabled by default.
return calculatedValue;
} }
} }

View file

@ -26,6 +26,9 @@ export default class AccountSettingHandler extends SettingsHandler {
// Special case URL previews // Special case URL previews
if (settingName === "urlPreviewsEnabled") { if (settingName === "urlPreviewsEnabled") {
const content = this._getSettings("org.matrix.preview_urls"); const content = this._getSettings("org.matrix.preview_urls");
// Check to make sure that we actually got a boolean
if (typeof(content['disable']) !== "boolean") return null;
return !content['disable']; return !content['disable'];
} }

View file

@ -40,11 +40,17 @@ export default class DeviceSettingsHandler extends SettingsHandler {
// Special case notifications // Special case notifications
if (settingName === "notificationsEnabled") { if (settingName === "notificationsEnabled") {
return localStorage.getItem("notifications_enabled") === "true"; const value = localStorage.getItem("notifications_enabled");
if (typeof(value) === "string") return value === "true";
return null; // wrong type or otherwise not set
} else if (settingName === "notificationBodyEnabled") { } else if (settingName === "notificationBodyEnabled") {
return localStorage.getItem("notifications_body_enabled") === "true"; const value = localStorage.getItem("notifications_body_enabled");
if (typeof(value) === "string") return value === "true";
return null; // wrong type or otherwise not set
} else if (settingName === "audioNotificationsEnabled") { } else if (settingName === "audioNotificationsEnabled") {
return localStorage.getItem("audio_notifications_enabled") === "true"; const value = localStorage.getItem("audio_notifications_enabled");
if (typeof(value) === "string") return value === "true";
return null; // wrong type or otherwise not set
} }
return this._getSettings()[settingName]; return this._getSettings()[settingName];

View file

@ -25,6 +25,9 @@ export default class RoomAccountSettingsHandler extends SettingsHandler {
// Special case URL previews // Special case URL previews
if (settingName === "urlPreviewsEnabled") { if (settingName === "urlPreviewsEnabled") {
const content = this._getSettings(roomId, "org.matrix.room.preview_urls"); const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
// Check to make sure that we actually got a boolean
if (typeof(content['disable']) !== "boolean") return null;
return !content['disable']; return !content['disable'];
} }

View file

@ -25,6 +25,9 @@ export default class RoomSettingsHandler extends SettingsHandler {
// Special case URL previews // Special case URL previews
if (settingName === "urlPreviewsEnabled") { if (settingName === "urlPreviewsEnabled") {
const content = this._getSettings(roomId, "org.matrix.room.preview_urls"); const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
// Check to make sure that we actually got a boolean
if (typeof(content['disable']) !== "boolean") return null;
return !content['disable']; return !content['disable'];
} }

View file

@ -116,7 +116,7 @@ export async function decryptMegolmKeyFile(data, password) {
aesKey, aesKey,
ciphertext, ciphertext,
); );
} catch(e) { } catch (e) {
throw friendlyError('subtleCrypto.decrypt failed: ' + e, cryptoFailMsg()); throw friendlyError('subtleCrypto.decrypt failed: ' + e, cryptoFailMsg());
} }

View file

@ -23,7 +23,7 @@ const localStorage = window.localStorage;
let indexedDB; let indexedDB;
try { try {
indexedDB = window.indexedDB; indexedDB = window.indexedDB;
} catch(e) {} } catch (e) {}
/** /**
* Create a new matrix client, with the persistent stores set up appropriately * Create a new matrix client, with the persistent stores set up appropriately

View file

@ -0,0 +1,68 @@
const React = require('react');
const expect = require('expect');
import * as languageHandler from '../../src/languageHandler';
const testUtils = require('../test-utils');
describe('languageHandler', function() {
let sandbox;
beforeEach(function(done) {
testUtils.beforeEach(this);
sandbox = testUtils.stubClient();
languageHandler.setLanguage('en').done(done);
});
afterEach(function() {
sandbox.restore();
});
it('translates a string to german', function() {
languageHandler.setLanguage('de').then(function() {
const translated = languageHandler._t('Rooms');
expect(translated).toBe('Räume');
});
});
it('handles plurals', function() {
const text = 'and %(count)s others...';
expect(languageHandler._t(text, { count: 1 })).toBe('and one other...');
expect(languageHandler._t(text, { count: 2 })).toBe('and 2 others...');
});
it('handles simple variable subsitutions', function() {
const text = 'You are now ignoring %(userId)s';
expect(languageHandler._t(text, { userId: 'foo' })).toBe('You are now ignoring foo');
});
it('handles simple tag substitution', function() {
const text = 'Press <StartChatButton> to start a chat with someone';
expect(languageHandler._t(text, {}, { 'StartChatButton': () => 'foo' }))
.toBe('Press foo to start a chat with someone');
});
it('handles text in tags', function() {
const text = '<a>Click here</a> to join the discussion!';
expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` }))
.toBe('xClick herex to join the discussion!');
});
it('variable substitution with React component', function() {
const text = 'You are now ignoring %(userId)s';
expect(languageHandler._t(text, { userId: () => <i>foo</i> }))
.toEqual((<span>You are now ignoring <i>foo</i></span>));
});
it('variable substitution with plain React component', function() {
const text = 'You are now ignoring %(userId)s';
expect(languageHandler._t(text, { userId: <i>foo</i> }))
.toEqual((<span>You are now ignoring <i>foo</i></span>));
});
it('tag substitution with React component', function() {
const text = 'Press <StartChatButton> to start a chat with someone';
expect(languageHandler._t(text, {}, { 'StartChatButton': () => <i>foo</i> }))
.toEqual(<span>Press <i>foo</i> to start a chat with someone</span>);
});
});